资讯详情

资讯详情

Python3爬虫学习入门教程

前言网络爬虫web crawler是按一定规则自动抓取网页的程序。它要解决的核心问题是把网页上人能看懂的信息变成程序能处理的结构化数据。入门阶段真正要掌握的不是某个第三方库而是三件事——怎么发一个合规的 HTTP 请求、怎么把返回的字节流还原成正确的字符串、怎么从字符串里把目标字段抽出来。一个典型误解是「爬虫就是发个请求再加个正则」。实际入门后最常翻车的恰恰是请求之外的那些细节不写 User-Agent 被服务器拒绝、不设超时导致程序卡死、对端返回的是 gzip 压缩内容却直接解码、页面声明的编码不是 UTF-8、跳转后相对链接拼接错。这些错误属于「程序能跑但结果是错的」比直接抛异常更难排查。本文只用Python 标准库把一条最小可用链路走通不需要安装任何第三方包。另外必须先说明Python 2 已于 2020 年 1 月 1 日停止维护本文一律使用 Python 3 写法。Python 2 里的urllib2.urlopen其能力在 Python 3 并入了urllib.request、print语句Python 3 里必须写成函数调用print(...)都不再可用。本文示例默认适用于 Python 3.8 及以上用到更新版本特性的地方会单独标注。一、先合规再写代码写第一行抓取代码之前有三件事必须先做否则程序写得再漂亮也是错的。第一是看目标站点的 robots.txt。它放在站点根目录声明了哪些路径允许抓、哪些不允许。Python 标准库提供了urllib.robotparser来解析它不要自己用字符串去切分。# 适用于 Python 3.8# crawl_delay 需要 Python 3.6 及以上import urllib.robotparserrp urllib.robotparser.RobotFileParser()rp.set_url(https://www.example.com/robots.txt)rp.read()print(rp.can_fetch(StudyCrawler, https://www.example.com/page)) # 允许返回 Trueprint(rp.crawl_delay(StudyCrawler)) # 无该指令时返回 Nonecan_fetch(useragent, url)的第一个参数是你的 User-Agent 标识第二个是要访问的 URL。crawl_delay(useragent)返回 Crawl-delay 的秒数没有就返回None——它需要 Python 3.6 及以上。关于 robots.txt 本身的结构以 RFC 9309 和官方文档为准。按 RFC 的建议若 robots.txt 返回 4xx 通常视为允许全部返回 5xx 应视为全部禁止稳妥起见拿不到文件时按「禁止」处理对自己更安全。第二是设置一个能识别身份的 User-Agent。默认的Python-urllib/3.x很容易被风控直接拦掉写清楚程序名和联系方式对方才好判断你是不是恶意的。第三是限速。请求之间加time.sleep()别让单个站点被你打满。抓取公开数据也要看目标站的服务条款登录后可见的数据、付费内容不要碰。二、发一个请求urllib.requesturllib.request.urlopen的签名是urlopen(url, dataNone, *, timeout, contextNone)。url可以是字符串也可以是一个Request对象。要自定义请求头就必须用Request它的签名是Request(url, dataNone, headers{}, origin_req_hostNone, unverifiableFalse, methodNone)。# 适用于 Python 3.8import urllib.requesturl https://www.example.com/req urllib.request.Request(url, headers{User-Agent: StudyCrawler/0.1 (contact: meexample.com),})with urllib.request.urlopen(req, timeout10) as resp:raw resp.read()print(状态码:, resp.status)print(内容类型:, resp.headers.get(Content-Type))几个必须记住的点urlopen返回的对象是上下文管理器用with包起来才能保证连接被释放。resp.read()返回的是bytes不是 str。官方文档明确说明因为无法自动判断字节流的编码。timeout参数务必显式传缺省是全局默认套接字超时线上环境很容易卡死。urlopen默认会跟随 HTTP 重定向。状态码不是 2xx 时不会返回响应对象而是抛urllib.error.HTTPError。三、把字节变成正确的字符串拿到 bytes 之后要解码。最常见的错误是硬编码.decode(utf-8)而页面实际编码可能是 GBK、GB2312 或 ISO-8859-1。正确做法是先看响应头的Content-Type从中取 charset。# 适用于 Python 3.8import urllib.requestreq urllib.request.Request(https://www.example.com/, headers{User-Agent: StudyCrawler/0.1 (contact: meexample.com),})with urllib.request.urlopen(req, timeout10) as resp:raw resp.read()charset resp.headers.get_content_charset() or utf-8text raw.decode(charset, errorsreplace)print(字符数:, len(text))resp.headers是http.client.HTTPMessage继承了 email 消息对象的行为get_content_charset()会从Content-Type里取出 charset 小写返回没有则返回None。errorsreplace保证遇到个别坏字节不会整段解码失败。另外如果响应是 gzip 压缩的urlopen不会自动解压需要自己用gzip模块处理——先确认响应头里有没有Content-Encoding: gzip。四、把字段抽出来html.parser标准库的html.parser.HTMLParser是事件驱动的解析器。用法是继承它并覆写回调方法然后调用feed(data)喂入 HTML 文本最后调用close()。它的构造签名是HTMLParser(*, convert_charrefsTrue, scriptingFalse)其中scripting是 3.14 才加入的为了兼容别传它。# 适用于 Python 3.8from html.parser import HTMLParserfrom urllib.parse import urljoinclass HrefGatherer(HTMLParser):先原样记住 a 标签的 href最后统一拼成绝对地址。def __init__(self, base):super().__init__(convert_charrefsTrue)self.base baseself.raw_hrefs []def handle_starttag(self, tag, attrs):self._remember_href(tag, attrs)def handle_startendtag(self, tag, attrs):self._remember_href(tag, attrs)def _remember_href(self, tag, attrs):if tag a:href dict(attrs).get(href)if href:self.raw_hrefs.append(href)propertydef links(self):return [urljoin(self.base, href) for href in self.raw_hrefs]parser HrefGatherer(https://www.example.com/dir/)parser.feed(a hrefa.htmlA/aa href/bB/a)parser.close()print(parser.links)handle_starttag(tag, attrs)里的attrs是(属性名, 属性值)元组的列表属性值是字符串或None布尔属性没有值。handle_data(data)负责文本节点。解析绝对不要把正则套在整页 HTML 上——HTML 不是正则语言嵌套和属性顺序一变化结果就错。实战抓一个列表页并保存把上面的片段串起来加上合规检查和限速就是一个最小的可用爬虫。# 适用于 Python 3.8import timeimport urllib.errorimport urllib.requestimport urllib.robotparserfrom html.parser import HTMLParserfrom urllib.parse import urljoin, urlsplitUSER_AGENT StudyCrawler/0.1 (contact: meexample.com)HEADERS {User-Agent: USER_AGENT}class HrefGatherer(HTMLParser):先原样记住 a 标签的 href最后统一拼成绝对地址。def __init__(self, base):super().__init__(convert_charrefsTrue)self.base baseself.raw_hrefs []def handle_starttag(self, tag, attrs):self._remember_href(tag, attrs)def handle_startendtag(self, tag, attrs):self._remember_href(tag, attrs)def _remember_href(self, tag, attrs):if tag a:href dict(attrs).get(href)if href:self.raw_hrefs.append(href)propertydef links(self):return [urljoin(self.base, href) for href in self.raw_hrefs]def robots_allows(url, user_agent):parts urlsplit(url)robots_url parts.scheme :// parts.netloc /robots.txtrp urllib.robotparser.RobotFileParser()rp.set_url(robots_url)try:rp.read()except urllib.error.URLError:return False # 拿不到 robots.txt保守处理return rp.can_fetch(user_agent, url)def fetch_text(url, timeout10):req urllib.request.Request(url, headersHEADERS)with urllib.request.urlopen(req, timeouttimeout) as resp:raw resp.read()charset resp.headers.get_content_charset() or utf-8return raw.decode(charset, errorsreplace)def main():queue [https://www.example.com/]seen set()crawled 0while queue and crawled 5:url queue.pop(0)if url in seen:continueseen.add(url)if not robots_allows(url, USER_AGENT):print(robots 不允许跳过:, url)continuetry:html fetch_text(url)except urllib.error.HTTPError as e:print(HTTP 错误, e.code, url)continueexcept urllib.error.URLError as e:print(网络错误, e.reason, url)continueparser HrefGatherer(url)parser.feed(html)parser.close()for link in parser.links:if link.startswith((http://, https://)) and link not in seen:queue.append(link)crawled 1time.sleep(1) # 限速每个请求之间等待 1 秒print(已抓取页面数:, crawled)if __name__ __main__:main()这个版本是广度优先的seen集合负责去重真实项目里应换成对规范化后的 URL 做哈希。限速用固定间隔简单但足够入门。常见坑点不写 User-Agent❌urllib.request.urlopen(url)—— 默认 UA 是Python-urllib/3.x容易被风控拦。 ✅ 用Request(url, headers{User-Agent: ...})标识程序身份。硬编码 UTF-8 解码❌resp.read().decode(utf-8)—— 页面是 GBK 时得到乱码或抛UnicodeDecodeError。 ✅raw.decode(resp.headers.get_content_charset() or utf-8, errorsreplace)。不设超时❌urlopen(url)—— 对端不响应时可能长时间阻塞。 ✅urlopen(req, timeout10)并对超时异常做处理。把 HTTPError 当普通错误忽略❌ 只捕获URLError——HTTPError虽是它的子类但会先被抛出不处理就中断循环。 ✅ 分别捕获HTTPError能读e.code和URLError能读e.reason。用正则或字符串切分解析 HTML❌html.split(title)[1].split(/title)[0]—— 大小写、属性顺序一变就崩。 ✅ 用html.parser.HTMLParser子类覆写回调。用字符串加法拼 URL❌base / href—— 重复斜杠、多余目录、../全都处理不对。 ✅urljoin(base, href)它按 RFC 规则解析相对地址。忽略 robots.txt / 无限速❌while True: urlopen(url)—— 高频请求违反站点规则也拖垮对端。 ✅ 先用RobotFileParser.can_fetch()判断请求之间time.sleep()。把iteritems()之类 Python 2 写法搬过来❌for k, v in d.iteritems():—— Python 3 已移除该方法。 ✅ 用d.items()返回视图不是列表。总结环节标准库工具关键点合规检查urllib.robotparsercan_fetch/crawl_delay拿不到就保守处理发请求urllib.request.urlopen/Request自定义 UA、显式timeout解码HTTPMessage.get_content_charset别硬编码 UTF-8加errorsreplace解析html.parser.HTMLParser覆写handle_starttag/handle_data拼 URLurllib.parse.urljoin不要字符串拼接限速time.sleep尊重目标站别打满入门阶段先把标准库这条链路走通理解「请求—解码—解析—去重—限速」这五步各自的作用再考虑引入第三方库提高效率。合规和礼貌不是可选项而是能不能长期抓下去的前提。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →