☰
从零到一:用requests正则抓取界面新闻列表并生成CSV
2026/10/8 20:39:13 网站建设 项目流程

爬虫新手学了一堆 requests、正则、CSV 的语法,最后发现最缺的是“一个能跑通的完整项目”。今天这个项目就是把界面新闻科技频道的文章列表抓下来,提取标题、发布时间、摘要(导语)和原文链接,最后统一导出成 CSV。整个过程只用 requests、re、csv 这三个 Python 标准库之外的东西,不引入 Scrapy 和 Selenium,代码量不大,但该讲的坑一个不少。

界面新闻这个网站的信息密度比较高,文章列表的数据都在服务端直接渲染好,不需要等 JS 异步加载,所以用最传统的“请求 HTML → 正则提取 → 落盘 CSV”套路就能完成。这种项目特别适合练手,因为你会接触到编码处理、UA 伪装、分页遍历、HTML 标签清洗这几个爬虫里的高频问题,每一步都有真实场景支撑,学会了不是空泛的语法知识。

我实测下来,从发起请求到拿到第一份 CSV,核心代码不到 120 行。整个流程没有黑魔法,只要你照着下面每一节的思路走一遍,就能自己复现。如果你连 requests 怎么装都还没搞清楚,也没关系,后面环境准备部分会一起说清楚。

1. 这项目到底在爬什么:需求拆解与技术选型

1.1 数据字段:比单纯爬标题多要了什么

表面上看需求就四个字段:标题、发布时间、摘要(导语)、原文链接。但里面其实藏着几个容易被新手忽略的问题。

先说标题和原文链接。这两个字段几乎永远绑定在一起,因为列表页里标题就是一个<a>标签的文本,链接就是同一标签的href属性。如果你只抓标题不抓链接,后面数据就成一个孤立文本,没法溯源。我在实际项目里见过很多人抓了一堆列表数据,复盘时发现根本不知道每一条对应的原文页面是哪个,导出结果基本等于废了。所以这里一开始就把两者成对提取。

发布时间和摘要(导语)稍微麻烦一点。新闻列表页里的时间和摘要并不像标题那样有统一的语义标签,它们通常会嵌在各种class的div、span里。更麻烦的是,很可能同一页里面还混着“评论数”“栏目名”这些噪音字段,正则如果写得太宽,会把不该要的东西也一起匹配进来。

所以我把提取目标定死为这四样:title(纯文本标题)、link(完整 URL)、pub_time(标准日期时间格式)、summary(去掉 HTML 标签后的导语文本)。导出 CSV 时表头也按照这四列来排,字段顺序稳定,后面做数据分析或者给别的程序消费都不用二次调整。

1.2 为什么选 requests + 正则,而不是 Scrapy 和 Selenium

写爬虫项目有个很实际的经验:工具越重,调试成本越高,跑起来越累。虽然 Scrapy 是很多人推荐的爬虫框架,但它的项目结构、中间件、Pipeline 这套体系对新手并不友好。你写个 10 行就能完成的功能,在 Scrapy 里要先建项目、写 Item、写 Spider、写 Pipeline,光配置文件就能绕晕。而且 Scrapy 的调试方式也不是print一下就行,要看日志和响应对象,理解成本高了一块。

Selenium 就更没必要。它需要额外下载浏览器驱动,启动浏览器窗口,占内存不说,速度还慢。界面新闻这种新闻资讯站,列表页 HTML 里直接就有完整数据,你甚至不需要等任何 Ajax 请求,直接requests.get就能拿到。这种场景用 Selenium 纯粹是杀鸡用牛刀,还会带来反爬风险——一个没有设置任何浏览器指纹的自动化浏览器一上来,可能比普通请求更容易触发风控。

所以我的选择是requests发请求,正则表达式做解析。requests 的 API 简单直接,对新手最友好;正则表达式虽然看起来吓人,但对于这种“结构相对固定、一次性抓取”的任务,反而是最灵活的方案——它不依赖页面 DOM 的层级关系,你只要写出一个能匹配目标区块的 pattern,就能稳定提取。

关于热词里出现的“xpath 爬虫 text 函数”,我也提一句:XPath 确实是另一种常用解析方式,适合页面结构非常稳定、你能明确看到 DOM 层级的情况。但这需要额外依赖lxml库,而且一旦页面结构调整,XPath 路径往往比正更容易失效。正则相反,它匹配的是文本特征,比如“标题一定出现在<h4>下的<a>标签里”这种模式,只要版面不大改,正则都能继续工作。

1.3 分页与频率控制:爬虫的边界感

列表页默认只展示第一页内容,要拿更多文章必须遍历分页。控制分页的策略很简单:观察 URL 规律,找到页码参数。多数新闻站是?page=2这种形式。确认总页数的方式也简单:先手动访问第二页,看有没有数据,然后访问第三页,一直探测到空列表为止。

这里必须提醒一点:频率控制不是可选项,而是必选项。我一般限制每页请求之间至少间隔 1 秒,不仅是给服务器留面子,也是降低自己 IP 被临时封禁的概率。请求扔出去之后加个time.sleep(1),成本约等于零,收益却很实在。

2. 请求和解析:代码里的关键细节逐个说

2.1 请求头:伪装成浏览器是第一步

很多网站不校验请求头也能返回 200,但一旦加了反爬策略,第一道关卡就是看你的User-Agent是不是一个常规浏览器的值。requests 库默认的 UA 是python-requests/x.x.x,服务器一看就知道你是脚本访问,直接 403 都有可能。

所以我会在HEADERS里至少设置三个字段:

HEADERS = { "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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", }

UA 里带不带操作系统版本无所谓,关键是看起来像一个真实的 Chrome 浏览器。如果之后发现还是被拦,再加Referer和Cookie。Referer 一般设置为网站首页即可,表明你是从站内跳转过来的,而不是直接访问。

还有一个细节:timeout参数一定要设。如果不设,请求一旦卡住,程序会无限等待,整个采集流程就挂在那里。我习惯设置timeout=10,如果 10 秒内没响应就抛异常,配合重试逻辑处理。

2.2 正则表达式:匹配结构的核心

先看一个典型的新闻列表页 HTML 结构,大致是这种模式:

<h4><a href="https://www.jiemian.com/article/12345.html">标题文本</a></h4> <div class="txt">这里是摘要导语的内容,可能会有一些 <span>标签</span> 在里面。</div> <div class="meta"> <span>2024-01-15 10:30</span> <span>栏目名称</span> </div>

当然真实页面不会这么干净,会有各种属性顺序和空白字符的差异。所以正则的关键是:只匹配你要的那一块,容忍空白和属性顺序的变化。

核心 pattern 我写成这样:

pattern = re.compile( r'<h4><a href="(?P<link>[^"]+)"[^>]*>(?P<title>[^<]+)</a></h4>' r'.*?' r'class="[^"]*txt[^"]*"[^>]*>(?P<summary>.*?)</.*?>' r'.*?' r'(?P<pub_time>\d{4}-\d{2}-\d{2} \d{2}:\d{2})', re.S )

逐个解释一下:

  • <h4><a href="...">是硬结构,标题一定在这里,不需要通配符海捞。
  • (?P<link>[^"]+)是命名捕获组,抓取href双引号内的所有内容。用[^"]+而不是.*?,是因为引号是链接的天然边界,这样不会误抓到下一个属性。
  • (?P<title>[^<]+)同理,<是文本的天然边界,只要标题里没有尖括号,这个匹配就是安全的。
  • class="[^"]*txt[^"]*"表示 class 属性值里包含 “txt” 这个关键字的标签。这个匹配方式比class="txt"更健壮,因为很多前端会写成class="news-txt"或class="txt summary"。
  • (?P<summary>.*?)是非贪婪匹配,抓取摘要内容。配合后面的</.*?>,表示遇到任意标签的闭合就停止,这样如果摘要里有多余的 HTML 子标签,也能正常截断。
  • (?P<pub_time>\d{4}-\d{2}-\d{2} \d{2}:\d{2})这是日期时间格式的模式匹配。\d{4}匹配四位数字,\d{2}匹配两位数字,整体对应2024-01-15 10:30这种常见新闻时间格式。

re.S这个标志必须加。它让.能匹配换行符,因为 HTML 源码里标签之间经常有缩进和换行。如果不加re.S,很多跨行的匹配会失败,正则返回空结果。

2.3 编码处理:乱码的根源

界面新闻这类网站如果用 GB 系编码,直接用resp.text解出来很可能是一堆乱码。很多新手在这里卡住,以为是正则写错了,其实是编码没处理对。

requests 库默认会用 HTTP 响应头里的charset来解码,但有些服务器不返回这个字段,或者返回的值不准。稳妥的做法是用resp.apparent_encoding让 requests 从响应字节内容里自动推测编码,然后再赋值给resp.encoding。

resp.encoding = resp.apparent_encoding html = resp.text

这样处理后,中文基本不会出现乱码。万一自动推测还不对,就去浏览器的开发者工具里看Network面板,找到响应头的Content-Type字段,手动指定编码。

2.4 CSV 导出:为什么用 utf-8-sig

CSV 导出这个步骤看起来简单,但有一个经典坑:你用默认utf-8写出来的 CSV,在 Excel 里直接打开会乱码,因为 Excel 默认用 ANSI 编码读取 CSV 文件,不认识 UTF-8 字节流。解决办法是用utf-8-sig编码写入,它会在文件开头加一个 BOM 头(字节顺序标记),Excel 看到这个标记就能正确识别为 UTF-8。

另一个细节是newline=""。在 Windows 上如果不传这个参数,csv.writer写入的时候每行后面会多一个空行,因为csv模块和文件对象的换行符冲突了。这个我早期吃过亏,导出的 CSV 行间全是空行,明明数据没问题但看着就是一坨。

3. 完整的可运行代码:从请求到 CSV 一把梭

3.1 环境准备

开始之前先把环境检查一遍。假设你已经装了 Python 3.8 或以上版本,打开终端执行:

pip install requests

只有 requests 需要单独装,正则的re和csv都是标准库,不用额外安装。验证安装是否成功:

python -c "import requests; print(requests.__version__)"

能输出版本号就说明环境没问题。

3.2 完整代码全文

下面这段代码就是整个项目的完整实现,我建议你先整体看一遍,再去看后面的逐段解析。

import time import re import csv import requests BASE_URL = "https://www.jiemian.com/lists/7.html" PAGE_PARAM = "?page={}" HEADERS = { "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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9", } OUTPUT_FILE = "jiemian_tech.csv" MAX_PAGES = 3 def fetch_page(url): resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code != 200: raise RuntimeError(f"请求失败: HTTP {resp.status_code}") resp.encoding = resp.apparent_encoding return resp.text def parse_items(html): pattern = re.compile( r'<h4><a href="(?P<link>[^"]+)"[^>]*>(?P<title>[^<]+)</a></h4>' r'.*?' r'class="[^"]*txt[^"]*"[^>]*>(?P<summary>.*?)</.*?>' r'.*?' r'(?P<pub_time>\d{4}-\d{2}-\d{2} \d{2}:\d{2})', re.S ) items = [] for match in pattern.finditer(html): title = clean_text(match.group("title")) summary = clean_text(match.group("summary")) link = match.group("link").strip() if link.startswith("//"): link = "https:" + link elif link.startswith("/"): link = "https://www.jiemian.com" + link pub_time = match.group("pub_time").strip() items.append({ "title": title, "link": link, "summary": summary, "pub_time": pub_time, }) return items def clean_text(text): text = re.sub(r"<[^>]+>", "", text) text = text.replace("\n", " ").replace("\r", " ") text = re.sub(r"\s+", " ", text) return text.strip() def write_csv(items): with open(OUTPUT_FILE, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["标题", "发布时间", "摘要", "原文链接"]) for item in items: writer.writerow([ item["title"], item["pub_time"], item["summary"], item["link"], ]) def main(): all_items = [] for page in range(1, MAX_PAGES + 1): url = BASE_URL + PAGE_PARAM.format(page) print(f"正在采集第 {page} 页: {url}") html = fetch_page(url) items = parse_items(html) print(f"第 {page} 页提取到 {len(items)} 条记录") all_items.extend(items) time.sleep(1) write_csv(all_items) print(f"全部完成,共 {len(all_items)} 条记录,已导出到 {OUTPUT_FILE}") if __name__ == "__main__": main()

3.3 核心代码逐段解析

fetch_page函数把请求逻辑单独抽出来,好处是后面如果遇到需要重试的情况,只要改这一个函数就行,不用在main里到处打补丁。resp.status_code != 200的判断不是形式主义,我遇到过一次网站临时返回 503,如果不检查状态码,后面会拿一个错误页面去跑正则,匹配不到任何数据还找不到原因。

parse_items是这个项目的灵魂。pattern.finditer返回所有匹配位置,不会只取第一条,所以一页里十篇文章能全部抓出来。如果你用search那就只能拿到第一条,记住了。

clean_text用来清洗标题和摘要。核心操作是re.sub(r"<[^>]+>", "", text),把 HTML 标签全部剥掉。摘要里如果留着<span>或者<em>标签,写入 CSV 后会非常难看,后续分析也很麻烦。\s+替换成单个空格,是去除多余空白的标准做法,让最终导出的文本更干净。

链接补全那段很多人会忽略。网站的href有时候是相对路径//www.jiemian.com/article/123.html或者/article/123.html,这种情况下你直接放进 CSV 会导致链接在浏览器里打不开。穷举几种情况并补全为完整 URL,是个值得养成的好习惯。

3.4 导出结果长什么样

运行之后,你会得到一个jiemian_tech.csv文件。用 WPS 或者 Excel 打开,表头是“标题、发布时间、摘要、原文链接”,下面是每一篇文章的对应数据。

一个值得检查的点是摘要(导语)字段。如果导出的摘要内容里还有大量杂质,多半是前端在摘要里嵌了图片懒加载的占位标签,或者是有多个子标签嵌套。这种情况建议在clean_text里多跑一轮 HTML 标签剥离,把</div>、</p>、</span>这类冗余标签也过滤掉。

4. 踩坑实录:高频问题与排查对照表

4.1 高频问题速查

这是我整理的一个排查对照表,基本覆盖了这类列表页爬虫最常见的状况。

现象可能原因解决方案
返回内容全是乱码响应编码识别错误设置resp.encoding = resp.apparent_encoding,或手动指定 gb2312/gbk
正则匹配为空页面结构变化 / 正则写错先把 HTML 保存到本地文件,肉眼检查目标区块的真实结构
HTTP 403 或 418User-Agent 太明显设置完整浏览器头,必要时加Referer
请求卡住不返回没有设置 timeout给requests.get加timeout=10
CSV 出现大量空行打开文件时没加newline=""open(OUTPUT_FILE, "w", newline="")
Excel 打开 CSV 乱码编码用了普通 utf-8改成utf-8-sig写入
同一条数据出现两次分页区间重叠 / 正则匹配到重复区块检查 URL 页码参数是否从 0 开始,或对抓到的链接去重
摘要字段抓到一堆标签清洗不彻底用re.sub(r"<[^>]+>", "", text)再跑一遍
采集到一半被封 IP请求频率过高增大sleep间隔,随机化请求间隔时间

4.2 页面改版时怎么快速调整正则

网站改版是每个爬虫开发者的噩梦。我的习惯是,正则写好后先跑一次,把 HTML 源码保存到本地文件,再去改正则,而不是每次都重新请求线上页面。

具体做法很简单:在fetch_page里加一行open("debug.html", "w", encoding="utf-8").write(html),把抓下来的页面存下来。然后用 VS Code 或浏览器打开这个本地文件,定位到你要匹配的新闻区块,用肉眼对比一下正则里的硬结构(比如<h4><a href=)和真实页面是否一致。多数情况下改版只是换了 class 名或者标签层级,正则稍微调一调,五分钟就能修好。

另一个技巧是先在浏览器开发者工具里执行一下当前正则可不可能匹配成功。在 Console 里输入document.querySelector('h4 a').href,就能快速拿到一个有效的链接,用来验证页面结构是否还是你预想的模式。

4.3 从列表页爬完,还要不要爬详情页

这个项目的标题里只要求列表页数据,所以没有安排详情页请求。但很多人会问:摘要有时候被截断了,要不要进详情页抓全文?

我的建议是:初始版本不要爬详情页。先保证列表页的数据准、数据全、导出的 CSV 格式稳定,再考虑扩展。详情页的请求量是列表页的十倍以上,被封的风险直线上升,而且你需要的四个字段列表页都已经给了,完全没有必要为了“更完整”去扩大攻击面。如果你真的需要全文,等列表页流程稳定后再增加一个消费者模式:读 CSV 里的链接,逐个请求详情页,并把正文聚合到原纪录里。这是一个单独的扩展项目,不该混在第一个版本里。

5. 合规底线与后续玩法扩展

5.1 爬虫的边界:不是能爬就能用

很多人第一次跑通爬虫后会有点兴奋,但我要在这里把丑话说在前面。项目能不能做是一回事,做了之后怎么用是另一回事。界面新闻这类资讯站点有它的版权和数据使用规范,个人学习和研究用途的采集频率必须控制在合理范围内,不能长时间高频请求,更不能把抓下来的内容打包二次分发。

实际操作中,我的原则是:

  • 严格遵守请求间隔,不并发轰炸。
  • 只采集公开页面能看到的数据,绝不尝试越权接口。
  • 抓下来的数据仅供个人分析学习,不用于任何商业用途。
  • 如果网站有明确的服务条款禁止爬取,直接换公开数据集练手。

爬虫是技术工具,跟我平时写脚本自动化办公没有本质区别。但工具落到具体场景就有规则要遵守。做一个讲规矩的爬虫开发者,比做一个只会写代码的更值得长期发展。

5.2 扩展方向一:增量采集与去重

当前这个脚本每次运行都是从头抓,然后把结果直接写进 CSV。如果用于长期跟踪科技频道的每日新闻,你会发现文件里全是重复数据。

解决方法不复杂。在main里加一个历史链接集合,读取旧 CSV 里的“原文链接”列,然后只保留这一次新采集且不在历史集合里的记录。把这一层逻辑加上,脚本就从“一次性工具”进化成了“订阅式采集器”。

代码示意:

def load_existing_links(): links = set() try: with open(OUTPUT_FILE, "r", encoding="utf-8-sig") as f: reader = csv.DictReader(f) for row in reader: links.add(row["原文链接"]) except FileNotFoundError: pass return links

在main里采集完所有页后,只把link not in existing_links的记录写进新文件。这样每天定时跑一次,积累的都是增量文章。

5.3 扩展方向二:从列表到详情页正文抓取

这是最自然的下一步。在现有 CSV 的“原文链接”基础上,追加一个函数,对每篇文章详情页发一次请求,提取正文标题和正文内容。逻辑上完全能复用fetch_page,解析部分换成详情页的正文正则即可。

只是要再次提醒:请求量会成倍增长,间隔时间要加大,至少 2 到 3 秒。你可以控制只抓最近一天的链接,而不是把整个 CSV 全部重新跑一遍。

5.4 扩展方向三:遇到 JS 动态渲染怎么办

目前界面新闻列表页是服务端渲染,所以这套代码能直接跑。但如果你想把这个技能迁移到别的网站,会遇到一种情况:列表数据根本不在初始 HTML 里,而是通过 Ajax 接口加载的。

这时候再去硬啃 HTML 就费劲了。正确姿势是打开浏览器开发者工具,切到 Network 面板,刷新页面,找到返回 JSON 的 XHR 请求。你会发现数据接口其实比 HTML 还规整——JSON 结构清晰,不需要正则,用resp.json()直接一把梭。

从对策上看,requests + JSON 接口的组合比 Selenium 轻量得多。只有遇到那种接口加密、需要模拟真实点击才能拿数据的场景,才考虑上 Selenium。这是后续进阶的方向了,但思路要先立住:能拿接口数据就不要用浏览器自动化。

5.5 关于 CSV 的后续数据处理思路

导出 CSV 只是第一步。拿到文件后你还可以做几件事:

  • 用 pandas 读取 CSV,统计科技频道一天的发文量趋势。
  • 按发布时间排序,找到高峰发文时段。
  • 把摘要列做分词,看当天科技频道的热点关键词是什么。

这些都是把“采集能力”转化为“分析能力”的自然延伸。爬虫只是工具,数据后面能做什么,才是真正拉开差距的地方。

我个人在实际操作中的体会是:这类列表页采集项目虽然不算难,但它是所有复杂爬虫的骨架。你把请求、编码、正则、去重、导出这一整套流程跑熟了,后面遇到图片站、商品站、资讯站,基本都是一个套路换皮。而且这个过程真正锻炼的是“遇到问题自己定位”的能力——写正则匹配不到的时候,保存 HTML 看看真实结构;写 CSV 乱码的时候,想明白编码和 BOM 的关系。这些经验在官方文档里找不到,只有亲手踩一遍坑才能沉淀下来。

最后再分享一个小技巧:所有跟网页结构相关的细节,都不要凭记忆写。项目刚开始的时候,先把目标网页完整保存一份,当作你的“结构快照”。之后无论是网站改版还是正则调整,都拿这个快照做基准对比,效率会高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询