ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scrapy论文爬虫实战:深度学习数据采集与反爬全解析

Scrapy论文爬虫实战:深度学习数据采集与反爬全解析 做了几年算法相关的开发我一直有个困扰想查某个方向的论文总是得在几个网站之间来回切换手动把标题、作者、摘要、引用数一条条整理到表格里。后来发现这个重复劳动完全可以用爬虫替代而且用 Scrapy 这种成熟的框架写出来的爬虫不仅跑得稳还方便后续增量更新。这篇博文就从一个实际项目出发讲讲怎么用 Scrapy 抓取 Deep Learning 领域的论文数据从站点分析、字段设计、反爬应对到数据落库完整走一遍。不管你是刚入门爬虫的 Python 开发者还是做算法研究、想快速收集论文信息做调研的人这篇文章都会有帮助。我会尽量把关键步骤和参数选择讲清楚附上可以直接参考的代码和配置也把实际踩过的坑一并列出来。1. 项目整体设计与思路拆解1.1 论文数据抓取到底解决什么问题先说说这个项目的初衷。做 Deep Learning 方向的研究或者工程落地通常需要盯住最新的模型结构、训练方法和数据集光靠平时刷 Twitter 和公众号远远不够真正一手的信息在论文里。但论文分发渠道很分散arXiv 有 pre-printOpenReview 有带评审意见的版本ACL Anthology 覆盖 NLP 会议CVPR/ICCV 等计算机视觉会议又在 IEEE 和 CVF 各自的站上。想把这些来源汇总在一起靠人工复制粘贴效率太低而且容易漏。我当时的典型场景是这样的团队准备调研某个子方向比如基于扩散模型的图像生成需要把近三年的相关论文全部列出来包含标题、作者、机构、摘要、关键词、引用数、PDF 链接、发表状态这些信息最后再按时间排序做成简报。这个需求听起来不复杂但如果手动操作几百篇论文靠手工整理至少得花两三天而且摘要和作者列表很容易复制出错。用爬虫来抓取核心是解决两个问题一个是批量获取另一个是结构化存储让后续筛选和统计成为可能。1.2 为什么选 Scrapy 而不是 requests BeautifulSoup很多人写爬虫第一反应是 requests 加上 BeautifulSoup简单页面几分钟就能搞定。但论文数据抓取这个场景有几个特点决定了用 Scrapy 会更合适第一数据规模大。Deep Learning 领域的论文数量以万为单位计算请求量上来之后requests 脚本的同步请求模式会非常慢哪怕用 threading 也要自己管理线程池、处理请求失败重试代码很快会变得难以维护。第二站点结构复杂。同一个字段在列表页可能只有标题和链接摘要和作者必须进详情页才能拿到。这种列表页到详情页的跳转关系用 Scrapy 的 Request 回调机制处理起来非常顺手parse 方法里直接 yield 一个新的 Request框架会自动帮你管理队列和并发。第三后续需要增量更新。论文每天都在增加爬虫不可能只跑一次。Scrapy 天然支持去重和断点续爬配合增量抓取的思路可以做到每次只抓新增的内容。第四Scrapy 的架构本身就是为爬虫设计的。中间件、Pipeline、扩展点都预留好了限速、重试、代理切换这些高频需求在 Scrapy 里都有对应的配置选项或者现成的扩展而不是每次都从零造轮子。当然Scrapy 的学习曲线比 requests 陡峭一些但一旦理解了 Request 回调、Item Pipeline、中间件这几个核心概念写起来反而更加省心。1.3 整体方案站点源、字段体系和数据流向这个项目的完整链路是Scrapy Spider 从目标论文站点抓取列表页和详情页数据解析出结构化字段后交给 Item PipelinePipeline 里做清洗、去重和存储最终落库到本地 SQLite 或者导出成 JSON/CSV。我最终选定的目标站点和数据源包括站点用途特点arXiv最主要的论文预印本库有 Atom API也可网页抓取量大且更新快OpenReview带评审意见的论文有官方 API部分页面动态加载ACL AnthologyNLP 方向的论文库页面结构规整适合解析Semantic Scholar论文引用数据提供 API 接口适合补充引用数在实际项目中我用 arXiv 作为主要数据源因为它覆盖了 Deep Learning 绝大部分前沿工作而且有比较规范的元数据。Semantic Scholar 用来补充引用数和作者机构信息。OpenReview 只在需要抓取评审意见时才会用到。字段设计上我一开始就把字段定得比较完整后面做分析和展示就不用来回补数据。核心字段有以下几项title论文标题authors作者列表可能包含机构abstract论文摘要published发表日期updated更新日期categories论文所属分类如 cs.CV、cs.CL、cs.LGpdf_urlPDF 下载链接abs_url论文详情页链接citation_count引用数从 Semantic Scholar 补充source来源站点标识这套字段体系在后续做关键词筛选、时间线统计、作者合作网络分析时都够用。2. 核心细节解析与实操要点2.1 Scrapy 项目的目录与模块划分开始写代码之前先把项目结构搭好。用命令行创建项目只需要一行命令scrapy startproject arxiv_papers这行命令会自动生成一个标准 Scrapy 项目结构。我习惯在此基础上再调整一下让代码更清晰arxiv_papers/ ├── scrapy.cfg └── arxiv_papers/ ├── __init__.py ├── items.py # 定义抓取字段 ├── middlewares.py # 自定义中间件代理、UA等 ├── pipelines.py # Item Pipeline做清洗和存储 ├── settings.py # 全局配置文件 ├── spiders/ │ ├── __init__.py │ └── arxiv_spider.py # 论文抓取核心逻辑 └── utils/ └── text_clean.py # 文本清洗函数很多人刚开始用 Scrapy 时会把所有逻辑都塞进一个 spider 文件里短期内没问题但后续加功能的时候会非常痛苦。我建议从一开始就按照 items、pipelines、middlewares、utils 四个维度拆开让每个模块只负责一件事。比如文本清洗单独放一个 utils 文件后面抓 OpenReview 或者其他站点时可以直接复用不用重复写。2.2 目标站点的结构分析与抓取策略以 arXiv 为例它的网页结构主要有两种抓取方式。第一种是直接用网页爬取。arXiv 的列表页 URL 格式非常规整比如https://arxiv.org/list/cs.LG/recent这个页面会列出最新提交的机器学习论文包含标题、作者、摘要预览。但列表页的摘要往往是截断的需要进入每个论文的详情页才能拿到完整内容。另一种方式是使用 arXiv 提供的 Atom APIhttp://export.arxiv.org/api/query?search_queryall:deeplearningstart0max_results100API 返回的是 XML 格式的元数据包括标题、作者、摘要、分类、发布日期等解析起来比 HTML 更稳定而且不用太担心页面改版。我在项目里优先用了 Atom API因为它的字段完整且规范不需要写复杂的 XPath 或 CSS 选择器。但这不意味着网页抓取没有用。有的论文数据源没有公开 API或者 API 需要申请权限这时候就不得不解析 HTML。另外OpenReview 的 Web 界面部分数据是动态加载的单纯靠 requests 抓不到需要配合 Playwright 或者 Splash 来渲染动态内容。这个点很多人问我在后面常见问题里单独展开。2.3 items.py 的字段定义与数据校验items.py 里定义的结构化字段是整个爬虫的数据契约。我在这个文件里用的是 Scrapy 的 Item 类配 Field 来声明字段import scrapy class ArxivPaperItem(scrapy.Item): title scrapy.Field() authors scrapy.Field() abstract scrapy.Field() published scrapy.Field() updated scrapy.Field() categories scrapy.Field() pdf_url scrapy.Field() abs_url scrapy.Field() citation_count scrapy.Field() source scrapy.Field()Field 本身并不限制数据类型真正的数据校验放在 Pipeline 里做。我通常在 Pipeline 里检查几个关键字段是否存在比如 title 和 abs_url如果为空就丢给日志告警。这样做的原因很直接爬虫跑起来可能是几小时的长时间任务如果过程中字段解析出错越早发现越容易定位否则等跑完才发现数据大量缺失再来排查就麻烦了。3. 实操过程与核心环节实现3.1 用 arXiv API 抓取论文列表与详情先来看最核心的 Spider 代码。基于 arXiv API 的爬虫逻辑非常直观构造查询 URL请求后解析 XML提取每个 entry再对需要详情的字段做补充请求。import scrapy from arxiv_papers.items import ArxivPaperItem class ArxivSpider(scrapy.Spider): name arxiv allowed_domains [arxiv.org, export.arxiv.org] def start_requests(self): base_url http://export.arxiv.org/api/query params { search_query: all:deep learning, start: 0, max_results: 100, sortBy: submittedDate, sortOrder: descending, } # 注意Scrapy 的 FormRequest 也可以但这里用 GET 直接拼接更合适 query_string .join(f{k}{v} for k, v in params.items()) url f{base_url}?{query_string} yield scrapy.Request(url, callbackself.parse_feed) def parse_feed(self, response): # 解析 Atom XML for entry in response.xpath(//*[local-name()entry]): item ArxivPaperItem() item[title] entry.xpath( .//*[local-name()title]/text() ).get(default).strip() item[abstract] entry.xpath( .//*[local-name()summary]/text() ).get(default).strip() item[published] entry.xpath( .//*[local-name()published]/text() ).get(default) item[updated] entry.xpath( .//*[local-name()updated]/text() ).get(default) # 处理作者列表和分类 authors entry.xpath(.//*[local-name()author]/*[local-name()name]/text()).getall() item[authors] [author.strip() for author in authors] categories entry.xpath(.//*[local-name()category]/term).getall() item[categories] categories # 提取 PDF 链接arXiv API 里的 link 标签有很多个取 type 为 text/html 的主链接 links entry.xpath(.//*[local-name()link]) pdf_url None abs_url None for link in links: link_type link.xpath(type).get() href link.xpath(href).get() if link_type text/html: abs_url href elif link_type application/pdf: pdf_url href item[pdf_url] pdf_url item[abs_url] abs_url item[source] arxiv yield item这段代码有几个设计点值得注意第一Atom XML 里标签带命名空间直接用//title取不到所以用了*[local-name()title]这种写法这也是解析 Atom/RSS 时最常见的坑。第二作者和分类字段是一对多的关系用getall()取完整列表而不是用get()只取第一个。数据完整性在这里很关键一篇论文有多位作者是常态如果只抓第一个作者后面做合著者分析就会出大问题。第三PDF 链接判断了application/pdf这个 type。arXiv 的 link 标签有好几个有 HTML 页面、有 PDF 文件如果不做筛选会把同一个链接重复存储。3.2 用 XPath 抓取 HTML 列表页拿不到完整摘要时的备选方案API 是最省事的方案但不是所有论文站都有 API。很多学术会议官网只有纯 HTML 页面甚至连分页都是 JavaScript 动态生成的。遇到这种情况就得回到 XPath 解析这条路。我以 ACL Anthology 举例。它的论文列表页结构相对稳定每篇论文在p classd-sm-flex align-items-stretch标签里标题在strong标签里作者链接在a标签里代码结构大致如下p classd-sm-flex align-items-stretch span stronga hrefhttps://aclanthology.org/2023.acl-long.1/Title of the Paper/a/strong br/ Author1, Author2, Author3 br/ spanAbstract.../span /span /p对应的 XPath 提取逻辑def parse_listing(self, response): papers response.xpath(//p[contains(class, d-sm-flex)]) for paper in papers: title paper.xpath(.//strong/a/text()).get(default).strip() relative_url paper.xpath(.//strong/a/href).get(default) abs_url response.urljoin(relative_url) authors paper.xpath(.//span[2]/text()).get(default).strip() if paper.xpath(.//span[2]) else abstract paper.xpath(.//span[2]/span/text()).get(default).strip() item ArxivPaperItem() item[title] title item[abs_url] abs_url item[authors] authors item[abstract] abstract item[source] acl_anthology yield item这些选择器在实际运行中经常需要微调因为网站改版或者页面结构微调都会影响提取结果。为了降低这种风险我会在写完选择器之后立刻用 Scrapy Shell 做验证scrapy shell https://aclanthology.org/events/acl-2023/在 shell 里试跑 XPath确认能取到正确数据再去改 Spider 代码。这一步看起来很常规但能省下大量反复调试的时间尤其是面对结构不熟悉的网站时。3.3 动态加载内容iframe 和 async 请求的处理热搜词里有几个很有意思比如 “scrapy playwright 动态 iframe” 和 “svg 爬虫”。这其实指向了同一个大类问题不少现代网站的数据不是直接在 HTML 里返回的而是通过 JavaScript 异步请求获取或者嵌在 iframe 里。我在抓 OpenReview 时就碰到了这个问题。OpenReview 的论文页面正文部分是动态渲染出来的而且部分数据来自 API 接口。直接用 Scrapy 的 Request 去请求页面拿到的 HTML 里根本没有论文摘要。处理方案大致有三种第一种找页面背后的 JSON API。打开浏览器的开发者工具切到 Network 面板刷新页面观察 XHR 请求很多时候会发现页面数据来自一个接口比如https://api.openreview.net/notes?forumxxxx。直接请求这个接口解析 JSON比渲染页面省时省力得多。OpenReview 官方其实有 API 文档优先用官方 API 是最好的选择。第二种用 Playwright 做动态渲染。当找不到直接的 JSON API 时就只能在无头浏览器里等页面加载完后再获取最终的 DOM。Scrapy 可以通过scrapy-playwright中间件来集成 Playwright。配置方式如下# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } PLAYWRIGHT_BROWSER_TYPE chromiumSpider 里对需要渲染的请求加参数def start_requests(self): yield scrapy.Request( urlhttps://openreview.net/forum?idxxxx, meta{playwright: True}, callbackself.parse_detail, )注意Playwright 渲染会显著降低爬取速度因为每个页面都要启动浏览器、加载资源、等待渲染完成。所以我在项目里只对确实需要渲染的路径启用 Playwright对普通页面仍然走默认的 Request 流程这样可以把性能开销控制在可接受范围内。第三种处理 iframe 嵌套。有些论文数据嵌在第三方服务的 iframe 里比如一些会议网站把论文 PDF 预览嵌在 iframe 中。iframe 的内容本质上是一次独立的页面加载在 Scrapy 里可以对 iframe 的 src 地址单独发一个 Request。要注意的坑是iframe 的 src 可能是相对路径需要使用response.urljoin()转成绝对地址。这三种方案的选择原则很明确优先找 JSON API其次用动态渲染最后才考虑 iframe 嵌套解析。顺序不能反因为动态渲染的成本远高于直接解析 JSON。3.4 Item Pipeline数据清洗、去重与落库Spider 拿到的是原始解析结果直接存数据库是不够的很多细节需要处理。我的 Pipeline 一般包含三个核心环节清洗去重、字段补全、存储归档。先看清洗去重。论文标题里经常有换行符、多余空格、全角字符这些都需要归一化。另外同一篇论文可能同时出现在 arXiv 和 OpenReview 上URL 和标题不完全一样但内容是同一篇单纯靠摘要相似度判断又会误判。我的策略是用“标题的小写 年份”作为主键来去重效果比较让人满意。import hashlib class CleanAndDeduplicatePipeline: def process_item(self, item, spider): # 标题清洗 if item.get(title): item[title] .join(item[title].split()) item[title] item[title].replace(\n, ).strip() # 摘要清洗 if item.get(abstract): item[abstract] .join(item[abstract].split()) # 生成唯一标识 title_key item.get(title, ).lower() year if item.get(published): year item[published][:4] unique_key f{title_key}|{year} item[_unique_key] hashlib.md5(unique_key.encode(utf-8)).hexdigest() return item然后将去重后的数据写入 SQLite。选择 SQLite 而不是 MySQL 的原因是项目数据量不大一般是几万条的量级单机 SQLite 完全够用而且零配置执行sqlite3命令就能直接查询不需要搭建额外的数据库服务。当然如果后续要部署成服务可以改成 MySQL 或者 PostgreSQL。存储 Pipeline 大致逻辑如下import sqlite3 class SQLitePipeline: def open_spider(self, spider): self.conn sqlite3.connect(papers.db) self.cursor self.conn.cursor() self.cursor.execute( CREATE TABLE IF NOT EXISTS papers ( id TEXT PRIMARY KEY, title TEXT, authors TEXT, abstract TEXT, published TEXT, updated TEXT, categories TEXT, pdf_url TEXT, abs_url TEXT, citation_count INTEGER, source TEXT ) ) def process_item(self, item, spider): self.cursor.execute( INSERT OR IGNORE INTO papers (id, title, authors, abstract, published, updated, categories, pdf_url, abs_url, citation_count, source) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( item.get(_unique_key), item.get(title), ,.join(item.get(authors, [])), item.get(abstract), item.get(published), item.get(updated), ,.join(item.get(categories, [])), item.get(pdf_url), item.get(abs_url), item.get(citation_count, 0), item.get(source), ) ) self.conn.commit() return item def close_spider(self, spider): self.conn.close()这里有一个细节值得讲在 SQLite 里作者和分类都是多值字段我选择用逗号拼接成字符串存储。这个方案对大多数分析场景够用但如果要统计作者合作网络就会比较痛苦。更好的方案是拆分成papers表和authors表用外键关联。我的第一个版本偷懒用了逗号拼接后来做作者共现分析时被迫重新写脚本拆分这种经验不推荐大家复制。如果你预判后续要做关系分析最好一开始就设计成两张表。4. 并发、限速、去重与反爬配置4.1 控制并发策略CONCURRENT_REQUESTS 到底设多大爬虫的一个核心矛盾是效率和反爬之间的平衡。Scrapy 的并发控制主要在 settings.py 里配置# settings.py CONCURRENT_REQUESTS 16 CONCURRENT_REQUESTS_PER_DOMAIN 4 CONCURRENT_REQUESTS_PER_IP 4 DOWNLOAD_DELAY 1.0这些参数的设置不能照抄网上的模板需要根据目标站点的承受能力和爬虫任务的性质来调整。我自己用过的一个经验值是对 arXiv 这样的学术站点设置CONCURRENT_REQUESTS_PER_DOMAIN 4、DOWNLOAD_DELAY 1既不会太快导致 IP 被临时限制也能在几小时内完成几千篇论文的抓取。有个细节很多人容易忽略CONCURRENT_REQUESTS_PER_DOMAIN和CONCURRENT_REQUESTS_PER_IP是有区别的。如果爬虫经过了 DNS 解析同一个域名会走到同一台 IP用哪个配置都一样。但如果有 CDN 或者域名解析到多 IP这两个参数的影响就不同了。一般来说CONCURRENT_REQUESTS_PER_DOMAIN更贴近实际控制维度优先调整它。再补充一个关于DOWNLOAD_DELAY的理解。这个参数不是每个请求之间严格间隔多少秒而是上一个请求结束到下一个请求开始的间隔。如果页面响应时间本来就长实际并发效果和参数设置之间会有一个偏差。所以更好的方式是配合AutoThrottle扩展做自适应限速AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY 4.0AutoThrottle会自动根据服务器的响应时间调整延迟目标是把并发控制在目标值附近。这个机制对论文站这类响应时间变化较大的场景很实用凌晨服务器响应快爬虫会自动加速白天服务器忙爬虫自动放慢不会因为一个瞬时的高延迟就把整个任务卡住。4.2 UA、IP 代理与请求头如何应对站点风控爬虫做久了会发现网站风控通常会先看 HTTP 请求的特征。第一个特征就是 User-Agent默认的Scrapy/2.x很容易被识别拦截。我在 settings.py 里统一配置了一个常用浏览器的 UAUSER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36但固定一个 UA 还是不够因为同一个 UA 反复高频访问也会被统计到指纹里。更好的做法是自己写一个 RandomUserAgentMiddleware从列表里随机选择 UA让请求特征更接近真实用户class RandomUserAgentMiddleware: def __init__(self): self.user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36, Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1, ] def process_request(self, request, spider): request.headers[User-Agent] random.choice(self.user_agents) return None然后是代理 IP 的问题。搜索热词里 “python 爬虫 ip代理” 出现频率很高说明大家在真实项目中都会遇到。但需要分清场景论文数据源比如 arXiv通常不会做特别严格的 IP 封禁只要控制好频率普通家用 IP 也能顺利跑完。真正需要代理的场景是一是目标站点对国外常规机房 IP 风控严格二是抓取规模非常大单 IP 的吞吐量不够。我用代理的策略是“够用就好”。如果目标站点能直连尽量不引入代理因为代理延迟、稳定性、成本都是额外的负担。如果确实需要选择支持 HTTP(S) 的代理池然后在 Middleware 里实现随机切换class ProxyMiddleware: def process_request(self, request, spider): proxy self.get_random_proxy() # 从代理池里取一个IP request.meta[proxy] proxy return None这里要提示一个重要坑如果在代理返回的响应里出现验证码或者跳转有时候并不是你的代理失效而是代理池里某些 IP 本身已经被目标站拦截了。遇到这种情况建议在日志里记录代理 IP 和响应的状态码对比查看哪些 IP 频繁触发风控把它们从池子里剔除。4.3 验证码和登录墙的边界处理热搜词里有“爬虫验证码”这也是爬虫领域最经典的问题之一。不过对于论文数据抓取这个具体项目我的态度是大部分学术数据源不需要破解验证码尽量不碰这个泥潭。arXiv 的 Atom API 完全不需要登录也不需要验证码。OpenReview 的公开论文列表也能匿名访问个别需要登录的功能可以通过官方 API 解决。Semantic Scholar API 只要申请一个免费 API Key就能覆盖大部分引用数据需求。真正的验证码风险通常出现在爬大众点评、淘宝、京东这类商业平台那些平台的验证码机制和学术论文站完全不是同一个量级。所以这个项目的经验其实是选择合适的数据源能有效绕开验证码问题。如果非抓不可的站点上了验证码优先看是否有官方 API 或者第三方数据服务验证码破解这件事风险高、收益低而且容易触犯站方规定不建议在没有授权的情况下折腾。4.4 断点续爬与增量更新让爬虫具备长期价值论文数据有个特点它是持续增长的今天抓完不代表以后不用管了。所以一个合格的论文爬虫必须具备两个能力断点续爬和增量更新。Scrapy 的断点续爬靠 Job 持久化实现。通过JOBDIR参数可以让爬虫在中断后恢复运行scrapy crawl arxiv -s JOBDIRcrawls/arxiv_search在JOBDIR指定的目录下Scrapy 会自动保存去重集合、待请求队列等状态。中断后的爬虫重新执行时能跳过已经处理过的 URL。增量更新则是通过time参数实现的。arXiv API 支持时间范围查询可以只抓取最近一天或者最近一周新增的论文search_querycat:cs.LG AND submittedDate:[202401010000 TO 202401080000]把上一次抓取的最大日期记在数据库里下次启动时从这个日期开始抓取就能实现真正的增量更新。我通常会写一个简单的调度脚本每天定时运行一次爬虫把新论文追加到数据库里这样论文库就一直是活的。5. 数据存储与归档论文数据的落库与后续分析5.1 JSON 导出与 SQLite 落库的取舍爬虫抓到的数据最终需要落地。最简单的方案是直接导出 JSONscrapy crawl arxiv -o papers.json这个方式适合项目初期调试但直接输出 JSON 的问题在于一次爬虫运行的输出会覆盖上一次运行的结果不方便做累积增量而且 JSON 文件过大时后续查询和筛选也不方便。我实际生产中采用的是 SQLite 落库方案原因前面提过零配置、单文件、直接支持 SQL 查询。建表时需要注意几个设计点给published字段加索引因为后续统计大概率会按时间过滤。给_unique_key加主键保证去重。abstract字段用TEXT类型因为论文摘要可能超过几千个字符。如果项目后期数据量超过百万级或者需要多人同时访问再迁移到 PostgreSQL 也不迟。迁移时表结构基本不用大改。5.2 数据清洗中的常见脏数据类型论文数据在抓取过程中会出现各种脏数据我总结下来主要这几类第一类是空白和格式问题。标题和摘要里经常有换行符、连续空格、全角括号等。这种问题在 Pipeline 里统一做一次 .join(text.split())就能解决顺便把\n替换成空格。第二类是字段缺失。某些论文没有摘要或者没有 PDF 链接这是正常情况。如果字段为空不要直接丢弃整条数据但要在日志中记录一下方便后续分析时知道哪些字段不可用。第三类是编码问题。学术论文库里偶尔会有特殊字符比如数学符号、希腊字母、Emoji 表情。SQLite 存储 UTF-8 编码时一般没问题但导出 CSV 并用 Excel 打开时很容易出现乱码。这个问题我后期的处理方案是导出 CSV 时统一用 UTF-8 编码并加 BOM这样 Excel 打开不会乱码。5.3 抓下来的论文数据还能做什么爬虫本身只是第一步数据抓下来之后的分析才是真正发挥价值的地方。用这些字段可以做不少事情关键词筛选针对abstract和title做关键词检索快速找出特定方向的最新论文。时间线统计按published统计每个月的论文数量看出某个子方向的热度变化趋势。作者合作网络解析authors字段构建作者之间的共现矩阵找出该领域的核心团队。引用分析结合 Semantic Scholar 的引用数据按引用数排序快速识别高影响力论文。构建知识库把摘要文本向量化持久化到向量数据库后续做 RAG 检索让模型在回答问题时可以参考真实论文内容。这些扩展方向让爬虫项目的价值远超过“抓一批论文”本身而是成为一个可持续更新的研究基础设施。6. 常见问题与排查技巧实录6.1 动态 iframe 与异步加载导致内容拿不到怎么办很多人在热搜词里问 scrapy playwright 动态 iframe 的问题这里集中说说。论文网站里动态加载一般分三种情况整个页面内容都由 JavaScript 渲染初始 HTML 里只有空壳。部分内容在 iframe 里iframe 的 src 指向另一个页面。数据来自异步接口页面加载后通过 XHR 请求获取。排查顺序建议是先在浏览器里右键“查看网页源代码”看看目标数据在不在源码里。如果在直接解析 HTML。如果不在打开 Network 面板找 XHR 请求看数据是不是 JSON API 返回的。如果 API 也找不到再考虑 Playwright 渲染。这样逐层递进能避免杀鸡用牛刀。6.2 被站点风控拦截后的快速处理爬虫跑了一段时间后响应里突然出现验证码或者返回 403、429 状态码大概率是被风控盯上了。这时候不要慌按下面这套流程排查先看响应的状态码。403 通常意味着 IP 或 UA 被拦截429 则说明请求太频繁。立刻降低并发增加DOWNLOAD_DELAY。我之前有个项目并发设得过高不到二十分钟就被封了后来把延迟调整到 3 秒才稳定跑完。检查一下 UA 是否被识别。有些站点有针对陌生 UA 的拦截策略换一个常用浏览器的 UA 往往能解决。确认是否启用了 Cookie。有的站根据会话行为判断是否真人保持一个统一的 Cookie 策略可能比频繁换头更有效。要注意的是出现一次 403 后不要立刻切换代理因为代理池里的 IP 质量不稳定切换后可能引发新的问题。先从频率控制开始调整是最稳妥的方案。6.3 并发过高导致数据质量问题并发太高不仅会触发反爬还可能导致数据抓取不完整。比如某一次请求超时没有拿到详情页的数据Item 里的 abstract 字段就是空的或者某个详情页请求被拒绝但 Spider 没有重试机制直接跳过了该条论文。应对方案是在 settings.py 里配置重试策略RETRY_ENABLED True RETRY_TIMES 3 RETRY_HTTP_CODES [500, 502, 503, 504, 522, 524, 408]注意这里没有把 403、404 加入重试列表。404 是页面不存在重试没有意义403 是权限问题重试也是同样的结果只有调整请求特征才可能解决。6.4 编码问题中文作者名和特殊符号乱码怎么办Deep Learning 论文有很大一部分作者来自非英语国家中文、日文、韩文名字虽然不多但确实存在而且摘要里也可能出现特殊符号比如\{a,b,c\}这类 LaTeX 公式字符。SQLite 和 UTF-8 没有问题但导出 CSV 时 Excel 的默认编码是 GBK 或 ANSI会显示乱码。我在项目中的处理方案是CSV 导出时指定编码为utf-8-sigimport csv with open(papers.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([title, authors, abstract, published]) # 逐条写入另外如果摘要里包含 LaTeX 格式的$...$或者数学符号在后续做文本处理时建议单独清洗因为这些符号对关键词匹配和向量化都会造成干扰。6.5 长期维护爬虫项目的几个心态建议最后一个算是非技术但很重要的经验。爬虫项目做出来容易真正难的是维护。论文网站的页面结构会改版API 可能会升级字段格式会调整这些都会导致爬虫失效。所以从项目一开始就要为维护留出余地第一代码里把站点解析逻辑集中到单独的函数或适配器中不要散落在各种回调里。改版时只需要改对应适配器不用动全局流程。第二给 Spider 加足够的日志输出。每次请求的状态码、解析到的字段条数、丢弃的数据量都记录下来。日志是最好的排查线索。第三定期检查数据质量。不要以为爬虫跑完就万事大吉偶尔抽查几条数据看看标题是否完整、摘要是否对得上就能及早发现解析规则失效的苗头。我在维护爬虫的这一年多里最深的体会是爬虫代码本身只占项目的小部分真正的工作量都在数据链路和工程化上——请求策略怎么设计、数据怎么清洗、失败了怎么重跑、怎么保证下一次启动还能跟上最新的数据。这几点想透了再复杂的论文数据需求也能稳稳拿下来。
返回列表