《Python网络数据采集.pdf》由会员分享,可在线阅读,更多相关《Python网络数据采集.pdf(80页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、 版权信息版权信息 书名:Python网络数据采集 作者:美 Ryan Mitchell 译者:陶俊杰 陈小莉 ISBN:978-7-115-41629-2 本书由北京图灵文化发展有限公司发行数字版。版权所有,侵权必究。本书由北京图灵文化发展有限公司发行数字版。版权所有,侵权必究。 您购买的图灵电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。 我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。 如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。 图灵社区会员 wsjjj(418836702) 专享 尊重版权 版权声明
2、OReilly Media, Inc. 介绍 业界评论 译者序 前言 什么是网络数据采集 为什么要做网络数据采集 关于本书 排版约定 使用代码示例 Safari Books Online 联系我们 致谢 第一部分 创建爬虫 第 1 章 初见网络爬虫 1.1 网络连接 1.2 BeautifulSoup简介 1.2.1 安装BeautifulSoup 1.2.2 运行BeautifulSoup 1.2.3 可靠的网络连接 第 2 章 复杂 HTML 解析 2.1 不是一直都要用锤子 2.2 再端一碗BeautifulSoup 2.2.1 BeautifulSoup的find()和findAll(
3、) 2.2.2 其他BeautifulSoup对象 2.2.3 导航树 2.3 正则表达式 2.4 正则表达式和BeautifulSoup 2.5 获取属性 2.6 Lambda表达式 2.7 超越BeautifulSoup 第 3 章 开始采集 3.1 遍历单个域名 3.2 采集整个网站 收集整个网站数据 3.3 通过互联网采集 3.4 用Scrapy采集 第 4 章 使用 API 4.1 API概述 4.2 API通用规则 4.2.1 方法 4.2.2 验证 4.3 服务器响应 API调用 4.4 Echo Nest 几个示例 4.5 Twitter API 4.5.1 开始 4.5.2
4、几个示例 4.6 Google API 4.6.1 开始 4.6.2 几个示例 4.7 解析JSON数据 4.8 回到主题 4.9 再说一点API 第 5 章 存储数据 5.1 媒体文件 5.2 把数据存储到CSV 5.3 MySQL 5.3.1 安装MySQL 5.3.2 基本命令 5.3.3 与Python整合 5.3.4 数据库技术与最佳实践 5.3.5 MySQL里的“六度空间游戏” 5.4 Email 第 6 章 读取文档 6.1 文档编码 6.2 纯文本 文本编码和全球互联网 6.3 CSV 读取CSV文件 6.4 PDF 6.5 微软Word和.docx 第二部分 高级数据采集
5、第 7 章 数据清洗 7.1 编写代码清洗数据 数据标准化 7.2 数据存储后再清洗 OpenRefine 第 8 章 自然语言处理 8.1 概括数据 8.2 马尔可夫模型 维基百科六度分割:终结篇 8.3 自然语言工具包 8.3.1 安装与设置 8.3.2 用NLTK做统计分析 8.3.3 用NLTK做词性分析 8.4 其他资源 第 9 章 穿越网页表单与登录窗口进行采集 9.1 Python Requests库 9.2 提交一个基本表单 9.3 单选按钮、复选框和其他输入 9.4 提交文件和图像 9.5 处理登录和cookie HTTP基本接入认证 9.6 其他表单问题 第 10 章 采集
6、 JavaScript 10.1 JavaScript简介 常用JavaScript库 10.2 Ajax和动态HTML 在Python中用Selenium执行JavaScript 10.3 处理重定向 第 11 章 图像识别与文字处理 11.1 OCR库概述 11.1.1 Pillow 11.1.2 Tesseract 11.1.3 NumPy 11.2 处理格式规范的文字 从网站图片中抓取文字 11.3 读取验证码与训练Tesseract 训练Tesseract 11.4 获取验证码提交答案 第 12 章 避开采集陷阱 12.1 道德规范 12.2 让网络机器人看起来像人类用户 12.2.
7、1 修改请求头 12.2.2 处理cookie 12.2.3 时间就是一切 12.3 常见表单安全措施 12.3.1 隐含输入字段值 12.3.2 避免蜜罐 12.4 问题检查表 第 13 章 用爬虫测试网站 13.1 测试简介 什么是单元测试 13.2 Python单元测试 测试维基百科 13.3 Selenium单元测试 与网站进行交互 13.4 Python单元测试与Selenium单元测试的选择 第 14 章 远程采集 14.1 为什么要用远程服务器 14.1.1 避免IP地址被封杀 14.1.2 移植性与扩展性 14.2 Tor代理服务器 PySocks 14.3 远程主机 14.3
8、.1 从网站主机运行 14.3.2 从云主机运行 14.4 其他资源 14.5 勇往直前 附录 A Python 简介 安装与“Hello,World!” 附录 B 互联网简介 附录 C 网络数据采集的法律与道德约束 C.1 商标、版权、专利 版权法 C.2 侵犯动产 C.3 计算机欺诈与滥用法 C.4 robots.txt和服务协议 C.5 三个网络爬虫 C.5.1 eBay起诉Bidders Edge与侵犯动产 C.5.2 美国政府起诉Auernheimer与计算机欺诈与滥用法 C.5.3 Field起诉Google:版权和robots.txt 作者简介 封面介绍 版权声明版权声明 201
9、5 by Ryan Mitchell. Simplified Chinese Edition, jointly published by OReilly Media, Inc. and Posts 因为每个 MySQL 实例可以有多个数据库,所以使用某个数据库之前需要指定数据库的名称: USE scraping; 从现在开始(直到关闭 MySQL 链接或切换到另一个数据库之前),所有的命令都运行在这个新的“scraping”数据库里面。 所有操作看着都非常简单。那么,在数据库里创建数据表的方法应该也类似吧?让我们在数据库里创建一个表来存储采集的网页: CREATE TABLE pages; 结
10、果显示错误: ERROR 1113 (42000): A table must have at least 1 column 和数据库不同,MySQL 数据表必须至少有一列,否则不能创建。为了在 MySQL 里定义字段(数据列),你必须在 CREATE TABLE 语句后面,把字段的定义放进一个带括号的、内部由 逗号分隔的列表中: CREATE TABLE pages (id BIGINT(7) NOT NULL AUTO_INCREMENT, title VARCHAR(200), content VARCHAR(10000), created TIMESTAMP DEFAULT CURRE
11、NT_TIMESTAMP, PRIMARY KEY (id); 每个字段定义由三部分组成: 名称(id 、title 、created 等) 数据类型(BIGINT(7) 、VARCHAR 、TIMESTAMP ) 其他可选属性(NOT NULL AUTO_INCREMENT ) 在字段定义列表的最后,还要定义一个“主键”(key)。MySQL 用这个主键来组织表的内容,便于后面快速查询。在本章后面的内容里,我将介绍如何调整这些主键以提高数据库的查询速度,但是现在, 用表的 id 列作为主键就可以。 语句执行之后,你可以用 DESCRIBE 查看数据表的结构: DESCRIBE pages;
12、+-+-+-+-+-+-+ | Field | Type | Null | Key | Default | Extra | +-+-+-+-+-+-+ | id | bigint(7) | NO | PRI | NULL | auto_increment | | title | varchar(200) | YES | | NULL | | | content | varchar(10000) | YES | | NULL | | | created | timestamp | NO | | CURRENT_TIMESTAMP | | +-+-+-+-+-+-+ 4 rows in set (
13、0.00 sec) 当然,这还是一个空表。你可以在 pages 表里插入一些测试数据,如下所示: INSERT INTO pages (title, content) VALUES (Test page title, This is some te st page content. It can be up to 10,000 characters long.); 需要注意的是,虽然 pages 表里有四个字段(id 、title 、content 、created ),但实际上你只需要插入两个字段(title 和 content )的数据即可。因为 id 字段是自动递增的(每次新插入 数据行
14、时 MySQL 自动增加 1),通常不用处理。另外,created 字段的类型是 timestamp ,默认就是数据加入时的时间戳。 当然,我们也可以自定义四个字段的内容: INSERT INTO pages (id, title, content, created) VALUES (3, Test page title, This is some test page content. It can be up to 10,000 characters long., 2014- 09-21 10:25:32); 只要你定义的整数在数据表的 id 字段里没有,它就可以顺利插入数据表。但是,这么做
15、非常不好;除非万不得已(比如程序中断漏了一行数据),否则让 MySQL 自己处理 id 和 timestamp 字段。 现在表里有一些数据了,你可以用很多方法来选择这些数据。下面是几个 SELECT 语句的示例: SELECT * FROM pages WHERE id = 2; 这条语句告诉 MySQL,“从 pages 表中把 id 字段中等于 2 的整行数据全挑选出来”。这个星号(* )是通配符,表示所有字段,这行语句会把满足条件(WHERE id = 2 )的所有字段都显示出来。如果 id 字段里没有任何一行等于 2,就会返回一个空集。例如,下面这个不区分大小写的查询,会返回 titl
16、e 字段里包含“test”的所有行(% 符号表示 MySQL 字符串通配符)的所有字段: SELECT * FROM pages WHERE title LIKE %test%; 但是,如果你的表有很多字段,而你只想返回部分字段怎么办?你可以不用星号,而用下面的方式: SELECT id, title FROM pages WHERE content LIKE %page content%; 这样就只会返回 title 字段包含“page content”的所有行的 id 和 title 两个字段了。 DELETE 语句语法与 SELECT 语句类似: DELETE FROM pages WH
17、ERE id = 1; 由于数据库的数据删除后不能恢复,所以在执行 DELETE 语句之前,建议用 SELECT 确认一下要删除的数据(本例中,就是用 SELECT * FROM pages WHERE id = 1 查看),然后把 SELECT * 换成 DELETE 就可以了,这会是一个好习惯。很多程序员都有过一些 DELETE 误操作的伤心往事,还有一些恐怖故事就是有人慌乱中忘了在语句中放 WHERE ,结果把所有客户数据都删除了。别让这种 事发生在你身上! 还有一个需要介绍的语句是 UPDATE : UPDATE pages SET title=A new title, content
18、=Some new content WHERE id=2; 结合本书的主题,后面我们就只用这些基本的 MySQL 语句,做一些简单的数据查询、创建和更新工作。如果你对这个强大数据库的命令和技术感兴趣,推荐你去看 Paul DuBois 的 MySQL Cookbook ( )。 5.3.3 与 与Python整合整合 Python 没有内置的 MySQL 支持工具。不过,有很多开源的库可以用来与 MySQL 做交互,Python 2.x 和 Python 3.x 版本都支持。最有名的一个库就是 PyMySQL( )。 写到这里的时候,PyMySQL 的版本是 0.6.2,你可以用下面的命令下载
19、并安装它: $ curl -L | tar xz $ cd PyMySQL-PyMySQL-f953785/ $ python setup.py install 如果需要更新,请检查最新版的 PyMySQL,并修改第一行下载链接中的版本号进行更新。 安装完成之后,你就可以使用 PyMySQL 包了。如果你的 MySQL 服务器处于运行状态,应该就可以成功地执行下面的命令(记得把 root 账户密码加进去): import pymysql conn = pymysql.connect(host=127.0.0.1, unix_socket=/tmp/mysql.sock, user=root,
20、passwd=None, db=mysql) cur = conn.cursor() cur.execute(USE scraping) cur.execute(SELECT * FROM pages WHERE id=1) print(cur.fetchone() cur.close() conn.close() 这段程序有两个对象:连接对象(conn )和光标对象(cur )。 连接 / 光标模式是数据库编程中常用的模式,不过刚刚接触数据库的时候,有些用户很难区分两种模式的不同。连接模式除了要连接数据库之外,还要发送数据库信息,处理回滚操作(当一个查询或一 组查询被中断时,数据库需要回到初
21、始状态,一般用事务控制手段实现状态回滚),创建新的光标对象,等等。 而一个连接可以有很多个光标。一个光标跟踪一种状态状态 (state)信息,比如跟踪数据库的使用状态。如果你有多个数据库,且需要向所有数据库写内容,就需要多个光标来处理。光标还会包含最后一次 查询执行的结果。通过调用光标函数,比如 cur.fetchone() ,可以获取查询结果。 用完光标和连接之后,千万记得把它们关闭。如果不关闭就会导致连接泄漏连接泄漏 (connection leak),造成一种未关闭连接现象,即连接已经不再使用,但是数据库却不能关闭,因为数据库不能确定你还要不 要继续使用它。这种现象会一直耗费数据库的资源
22、,所以用完数据库之后记得关闭连接! 刚开始的时候,你最想做的事情可能就是把采集的结果保存到数据库里。让我们用前面维基百科爬虫的例子来演示一下如何实现数据存储。 在进行网络数据采集时,处理 Unicode 字符串是很痛苦的事情。默认情况下,MySQL 也不支持 Unicode 字符处理。不过你可以设置这个功能(这么做会增加数据库的占用空间)。因为在维基百科上我们 难免会遇到各种各样的字符,所以最好一开始就让你的数据库支持 Unicode: ALTER DATABASE scraping CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; A
23、LTER TABLE pages CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE pages CHANGE title title VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE pages CHANGE content content VARCHAR(10000) CHARACTER SET utf8mb4 CO LLATE utf8mb4_unicode_ci; 这四行语句改变的内容有:数据库
24、、数据表,以及两个字段的默认编码都从 utf8mb4 (严格说来也属于 Unicode,但是对大多数 Unicode 字符的支持都非常不好)转变成了 utf8mb4_unicode_ci 。 你可以在 title 或 content 字段中插入一些德语变音符(umlauts)或汉语字符,如果没有错误就表示转换成功了。 现在数据库已经准备好接收维基百科的各种信息了,你可以用下面的程序来存储数据: from urllib.request import urlopen from bs4 import BeautifulSoup import re import datetime import ran
25、dom import pymysql conn = pymysql.connect(host=127.0.0.1, unix_socket=/tmp/mysql.sock, user=root, passwd=None, db=mysql, charset=utf8) cur = conn.cursor() cur.execute(USE scraping) random.seed(datetime.datetime.now() def store(title, content): cur.execute(INSERT INTO pages (title, content) VALUES (%
26、s, %s), (title, content) mit() def getLinks(articleUrl): html = urlopen(http:/en.wikipedia.org+articleUrl) bsObj = BeautifulSoup(html) title = bsObj.find(h1).get_text() content = bsObj.find(div, id:mw-content-text).find(p).get_text() store(title, content) return bsObj.find(div, id:bodyContent).findA
27、ll(a, href=pile(/wiki/)(?!:).)*$) links = getLinks(/wiki/Kevin_Bacon) try: while len(links) 0: newArticle = linksrandom.randint(0, len(links)-1).attrshref print(newArticle) links = getLinks(newArticle) finally: cur.close() conn.close() 这里有几点需要注意:首先,charset=utf8 要增加到连接字符串里。这是让连接 conn 把所有发送到数据库的信息都当成
28、UTF-8 编码格式(当然,前提是数据库默认编码已经设置成 UTF-8)。 然后要注意的是 store 函数。它有两个参数:title 和 content ,并把这两个参数加到了一个 INSERT 语句中并用光标执行,然后用光标进行连接确认。这是一个让光标与连接操作分离的好例子; 当光标里存储了一些数据库与数据库上下文(context)的信息时,需要通过连接的确认操作先将信息传进数据库,再将信息插入数据库。 最后要注意的是,finally 语句是在程序主循环的外面,代码的最底下。这样做可以保证,无论程序执行过程中如何发生中断或抛出异常(当然,因为网络很复杂,你得随时准备遭遇异常),光标和连 接
29、都会在程序结束前立即关闭。无论你是在采集网络,还是处理一个打开连接的数据库,用 try.finally 都是一个好主意。 虽然 PyMySQL 规模并不大,但是里面有一些非常实用的函数本书并没有介绍。具体请参考 Python 的 DBAPI 标准文档(http:/legacy.python.org/dev/peps/pep-0249/ )。 5.3.4 数据库技术与最佳实践 数据库技术与最佳实践 有些人的整个职业生涯都在学习、优化和创造数据库。我不是这类人,这本书也不是那类书。但是,和计算机科学的很多主题一样,有一些技巧你其实可以很快地学会,它们可以让你的数据库变得更高 效,让应用的运行速度更
30、快。 首先,给每个数据表都增加一个 id 字段,不会出什么问题。MySQL 里所有的表都至少有一个主键(就是 MySQL 用来排序的字段),因此 MySQL 知道怎么组织主键,通常数据库很难智能地选择主键。 究竟是用人造的 id 字段作为主键,还是用那些具有唯一性属性的字段作为主键,比如 username 字段,数据科学家和软件工程师已经争论了很多年,但我更倾向于主动创建一个 id 字段。这样做的原 因一两句话难以说清,不过对于一些非企业级系统的数据库,你还是应该用自增的 id 字段作为主键。 其次,用智能索引。字典(指的是常用的工具书,不是指 Python 的字典对象)是按照字母顺序排列的单
31、词表。这样做让你在任何时候都能快速地找到一个单词,只要你知道这个单词是如何拼写的就行。 你还可以把字典想象成另一种形式,将单词按照单词含义的字母顺序进行排列。如果你没玩过一些奇怪的游戏,比如危险边缘(Jeopardy)智力游戏,就不能理解游戏的含义,这样的字典就没法用了。但 是在数据库查询的工作里,这种按照字段含义进行排序的情况时有发生。比如,你的数据库里可能有一个字段经常要查询: SELECT * FROM dictionary WHERE definition=A small furry animal that says meow; +-+-+-+ | id | word | defini
32、tion | +-+-+-+ | 200 | cat | A small furry animal that says meow | +-+-+-+ 1 row in set (0.00 sec) 你可能非常想给这个表增加一个额外的键(除了已经存在的主键 id 之外),让查询变得更快。但是,增加额外的索引需要占用更多的空间,而且插入新行的时候也需要花费更多的时间。为了让事情简 单点儿,你可以让 MySQL 只检索查询列的一部分字符。比如下面的命令创建了一个查询 definition 字段前 16 个字符的智能索引: CREATE INDEX definition ON dictionary (
33、id, definition(16); 这个索引比全文查询的速度要快很多,而且不需要占用过多的空间和处理时间。 最后一点是关于数据查询时间和数据库空间的问题。一个常见的误区就是在数据库中存储大量重复数据,尤其是在做大量自然语言数据的网络数据采集任务时。举个例子,假如你想统计网站突然出现的 一些词组的频率。这些词组也许可以从一个现成的列表里获得,也许可以通过文本分析算法自动提取。最终你可能会把词组储存成下表的形式: +-+-+-+-+-+-+ | Field | Type | Null | Key | Default | Extra | +-+-+-+-+-+-+ | id | int(11)
34、| NO | PRI | NULL | auto_increment | | url | varchar(200) | YES | | NULL | | | phrase | varchar(200) | YES | | NULL | | +-+-+-+-+-+-+ 每当你发现一个词组就在数据库中增加一行,同时把 URL 记录下来。但是,如果把这些数据分成三个表,你就可以看到数据库占用的空间会大大降低: DESCRIBE phrases +-+-+-+-+-+-+ | Field | Type | Null | Key | Default | Extra | +-+-+-+-+-+-+ | i
35、d | int(11) | NO | PRI | NULL | auto_increment | | phrase | varchar(200) | YES | | NULL | | +-+-+-+-+-+-+ DESCRIBE urls +-+-+-+-+-+-+ | Field | Type | Null | Key | Default | Extra | +-+-+-+-+-+-+ | id | int(11) | NO | PRI | NULL | auto_increment | | url | varchar(200) | YES | | NULL | | +-+-+-+-+-+-
36、+ DESCRIBE foundInstances +-+-+-+-+-+-+ | Field | Type | Null | Key | Default | Extra | +-+-+-+-+-+-+ | id | int(11) | NO | PRI | NULL | auto_increment | | urlId | int(11) | YES | | NULL | | | phraseId | int(11) | YES | | NULL | | | occurrences | int(11) | YES | | NULL | | +-+-+-+-+-+-+ 虽然表定义的结构变复杂了
37、,但是新增的字段就是 id 字段。它们是整数,不会占用很多空间。另外,每个 URL 和词组都只会储存一次。 除非你安装了第三方包或保存详细的数据库日志,否则你无法掌握数据库里数据增加、更新或删除的具体时间。因此,如果需要对数据可用的空间、变更的频率和变更的重要性进行分析,你应该考虑在 数据新增、更新或删除时加一个时间戳。 5.3.5 MySQL里的里的“六度空间游戏六度空间游戏” 在第 3 章,我们介绍过“维基百科六度分隔”问题,其目标是通过一些词条链接寻找两个词条间的联系(即找出一条链接路径,只要点击链接就可以从一个维基词条到另一个维基词条)。 为了解决这个问题,我们不仅需要建立网络爬虫采集
38、网页(之前我们已经做过),还要把采集的信息以某种形式存储起来,以便后续进行数据分析。 前面介绍过的自增的 id 字段、时间戳以及多份数据表在这里都要用到。为了确定最合理的信息存储方式,你需要先想想游戏规则。一个链接可以轻易地把页面 A 连接到页面 B。同样也可以轻易地把页 面 B 连接到页面 A,不过这可能是另一条链接。我们可以这样识别一个链接,即“页面 A 存在一个链接,可以连接到页面 B”。也就是 INSERT INTO links (fromPageId, toPageId) VALUES (A, B); (其中,“A”和“B”分别表示页面的 ID 号)。 因此需要设计一个带有两张数据表
39、的数据库来分别存储页面和链接,两张表都带有创建时间和独立的 ID 号,代码如下所示: CREATE TABLE wikipedia.pages ( id INT NOT NULL AUTO_INCREMENT, url VARCHAR(255) NOT NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id); CREATE TABLE wikipedia.links ( id INT NOT NULL AUTO_INCREMENT, fromPageId INT NULL, toPageId IN
40、T NULL, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id); 注意,这里和前面打印页面标题的爬虫不同,我没有在页面数据表里使用页面标题字段,为什么这么做呢?其实是因为页面标题要在你进入页面后读取内容才能抓到。那么,如果我们想创建一个高效的 爬虫来填充这些数据表,那么只存储页面的链接就可以保存词条页面了,甚至不需要访问词条页面。 当然并不是所有网站都具有这个特点,但是维基百科的词条链接和对应的页面标题是可以通过简单的操作进行转换的。例如,http:/en.wikipedia.org/wiki/Mon
41、ty_Python 的后面就是页面标题“Monty Python”。 下面的代码会把“贝肯数”(一个页面与凯文 贝肯词条页面的链接数)不超过 6 的维基百科页面存储起来: from urllib.request import urlopen from bs4 import BeautifulSoup import re import pymysql conn = pymysql.connect(host=127.0.0.1, unix_socket=/tmp/mysql.sock,user= root, passwd=None, db=mysql, charset=utf8) cur = co
42、nn.cursor() cur.execute(USE wikipedia) def insertPageIfNotExists(url): cur.execute(SELECT * FROM pages WHERE url = %s, (url) if cur.rowcount = 0: cur.execute(INSERT INTO pages (url) VALUES (%s), (url) mit() return cur.lastrowid else: return cur.fetchone()0 def insertLink(fromPageId, toPageId): cur.e
43、xecute(SELECT * FROM links WHERE fromPageId = %s AND toPageId = %s, (int(fromPageId), int(toPageId) if cur.rowcount = 0: cur.execute(INSERT INTO links (fromPageId, toPageId) VALUES (%s, %s), (int(fromPageId), int(toPageId) mit() pages = set() def getLinks(pageUrl, recursionLevel): global pages if re
44、cursionLevel 4: return; pageId = insertPageIfNotExists(pageUrl) html = urlopen(http:/en.wikipedia.org+pageUrl) bsObj = BeautifulSoup(html) for link in bsObj.findAll(a, href=pile(/wiki/)(?!:).)*$): insertLink(pageId, insertPageIfNotExists(link.attrshref) if link.attrshref not in pages: # 遇到一个新页面,加入集合并搜索里面的词条链接 newPage = link.attrshref pages.add(newPage) getLinks(newPage, recursionLevel+1) getLinks(/wiki/Kevin_Bacon, 0) cur.close() conn.close() 用递归实