news 2026/10/3 13:10:45

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy论文爬虫实战:深度学习数据采集与反爬全解析

做了几年算法相关的开发,我一直有个困扰:想查某个方向的论文,总是得在几个网站之间来回切换,手动把标题、作者、摘要、引用数一条条整理到表格里。后来发现这个重复劳动完全可以用爬虫替代,而且用 Scrapy 这种成熟的框架,写出来的爬虫不仅跑得稳,还方便后续增量更新。这篇博文就从一个实际项目出发,讲讲怎么用 Scrapy 抓取 Deep Learning 领域的论文数据,从站点分析、字段设计、反爬应对到数据落库,完整走一遍。

不管你是刚入门爬虫的 Python 开发者,还是做算法研究、想快速收集论文信息做调研的人,这篇文章都会有帮助。我会尽量把关键步骤和参数选择讲清楚,附上可以直接参考的代码和配置,也把实际踩过的坑一并列出来。

1. 项目整体设计与思路拆解

1.1 论文数据抓取到底解决什么问题

先说说这个项目的初衷。做 Deep Learning 方向的研究或者工程落地,通常需要盯住最新的模型结构、训练方法和数据集,光靠平时刷 Twitter 和公众号远远不够,真正一手的信息在论文里。但论文分发渠道很分散,arXiv 有 pre-print,OpenReview 有带评审意见的版本,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 Pipeline,Pipeline 里做清洗、去重和存储,最终落库到本地 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.LG)
  • pdf_url:PDF 下载链接
  • 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 API:

http://export.arxiv.org/api/query?search_query=all:deep+learning&start=0&max_results=100

API 返回的是 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, callback=self.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 class="d-sm-flex align-items-stretch">标签里,标题在<strong>标签里,作者链接在<a>标签里,代码结构大致如下:

<p class="d-sm-flex align-items-stretch"> <span> <strong><a href="https://aclanthology.org/2023.acl-long.1/">Title of the Paper</a></strong> <br/> Author1, Author2, Author3 <br/> <span>Abstract...</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?forum=xxxx。直接请求这个接口,解析 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 = "chromium"

Spider 里对需要渲染的请求加参数:

def start_requests(self): yield scrapy.Request( url="https://openreview.net/forum?id=xxxx", meta={"playwright": True}, callback=self.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.0

AutoThrottle会自动根据服务器的响应时间调整延迟,目标是把并发控制在目标值附近。这个机制对论文站这类响应时间变化较大的场景很实用:凌晨服务器响应快,爬虫会自动加速;白天服务器忙,爬虫自动放慢,不会因为一个瞬时的高延迟就把整个任务卡住。

4.2 UA、IP 代理与请求头:如何应对站点风控

爬虫做久了会发现,网站风控通常会先看 HTTP 请求的特征。第一个特征就是 User-Agent,默认的Scrapy/2.x很容易被识别拦截。我在 settings.py 里统一配置了一个常用浏览器的 UA:

USER_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 JOBDIR=crawls/arxiv_search

在JOBDIR指定的目录下,Scrapy 会自动保存去重集合、待请求队列等状态。中断后的爬虫重新执行时,能跳过已经处理过的 URL。

增量更新则是通过time参数实现的。arXiv API 支持时间范围查询,可以只抓取最近一天或者最近一周新增的论文:

search_query=cat:cs.LG AND submittedDate:[202401010000 TO 202401080000]

把上一次抓取的最大日期记在数据库里,下次启动时从这个日期开始抓取,就能实现真正的增量更新。我通常会写一个简单的调度脚本,每天定时运行一次爬虫,把新论文追加到数据库里,这样论文库就一直是活的。

5. 数据存储与归档:论文数据的落库与后续分析

5.1 JSON 导出与 SQLite 落库的取舍

爬虫抓到的数据最终需要落地。最简单的方案是直接导出 JSON:

scrapy 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 状态码,大概率是被风控盯上了。这时候不要慌,按下面这套流程排查:

  1. 先看响应的状态码。403 通常意味着 IP 或 UA 被拦截,429 则说明请求太频繁。
  2. 立刻降低并发,增加DOWNLOAD_DELAY。我之前有个项目并发设得过高,不到二十分钟就被封了,后来把延迟调整到 3 秒才稳定跑完。
  3. 检查一下 UA 是否被识别。有些站点有针对陌生 UA 的拦截策略,换一个常用浏览器的 UA 往往能解决。
  4. 确认是否启用了 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-sig:

import csv with open("papers.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["title", "authors", "abstract", "published"]) # 逐条写入

另外,如果摘要里包含 LaTeX 格式的$...$或者数学符号,在后续做文本处理时建议单独清洗,因为这些符号对关键词匹配和向量化都会造成干扰。

6.5 长期维护爬虫项目的几个心态建议

最后一个算是非技术但很重要的经验。爬虫项目做出来容易,真正难的是维护。论文网站的页面结构会改版,API 可能会升级,字段格式会调整,这些都会导致爬虫失效。所以从项目一开始就要为维护留出余地:

第一,代码里把站点解析逻辑集中到单独的函数或适配器中,不要散落在各种回调里。改版时只需要改对应适配器,不用动全局流程。

第二,给 Spider 加足够的日志输出。每次请求的状态码、解析到的字段条数、丢弃的数据量都记录下来。日志是最好的排查线索。

第三,定期检查数据质量。不要以为爬虫跑完就万事大吉,偶尔抽查几条数据,看看标题是否完整、摘要是否对得上,就能及早发现解析规则失效的苗头。

我在维护爬虫的这一年多里,最深的体会是:爬虫代码本身只占项目的小部分,真正的工作量都在数据链路和工程化上——请求策略怎么设计、数据怎么清洗、失败了怎么重跑、怎么保证下一次启动还能跟上最新的数据。这几点想透了,再复杂的论文数据需求也能稳稳拿下来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:10:26

基因家族Motif分析全流程:从序列清洗到可视化及交叉验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:08:47

Dev-C++中文版安装与配置全指南:从汉化到C++17编译避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:06:49

六种水果分级数据集构建:从分级标准到模型部署全流程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:04:35

2026云栖大会AI原生技术落地实战:Agent云架构部署与产业落地全指南

摘要&#xff1a;2026年云栖大会彻底终结了大模型参数竞赛的行业旧范式&#xff0c;正式确立AI原生产业规模化落地的全新赛道。本文基于第一性原理拆解本次大会全部核心技术成果&#xff0c;从底层芯片算力、云端基础设施、大模型迭代、智能体架构到千行百业实战落地&#xff0…

作者头像 李华