Skip to content

Latest commit

 

History

History
645 lines (443 loc) · 26.4 KB

08.md

File metadata and controls

645 lines (443 loc) · 26.4 KB

八、搜索、挖掘和可视化数据

在本章中,我们将介绍:

  • 对 IP 地址进行地理编码
  • 收集维基百科编辑的 IP 地址
  • 在维基百科上可视化贡献者位置频率
  • 从 StackOverflow 作业列表创建 word cloud
  • 在维基百科上爬行链接
  • 在维基百科上可视化页面关系
  • 计算维基百科页面之间的分离度

介绍

在本章中,我们将研究如何搜索 web 内容、导出分析结果以及可视化这些结果。我们将学习如何定位内容海报并可视化其位置的分布。然后我们将研究如何在维基百科上刮取、建模和可视化页面之间的关系。

对 IP 地址进行地理编码

地理编码是将地址转换为地理坐标的过程。这些地址可以是实际的街道地址,可以使用各种工具进行地理编码,如谷歌地图地理编码 API(https://developers.google.com/maps/documentation/geocoding/intro )。IP 地址可以而且通常由各种应用程序进行地理编码,以确定计算机及其用户所在的位置。一个非常常见和有价值的用途是分析 web 服务器日志,以确定网站用户的来源。

这是可能的,因为 IP 地址不仅在能够与计算机通信方面表示计算机的地址,而且通常还可以通过在 IP 地址/位置数据库中查找来转换为近似的物理位置。有许多这样的数据库可用,所有这些数据库都由不同的注册商(如 ICANN)维护。还有其他工具可以报告公共 IP 地址的地理位置。

有许多免费的 IP 地理定位服务。我们将研究一个非常容易使用的,freegoip.net。

准备

Freegeoip.net 是一个免费的地理编码服务。如果你去http://www.freegeoip.net 在浏览器中,您将看到一个类似于以下内容的页面:

The freegeoip.net home page

默认页面报告您的公共 IP 地址,并根据其数据库提供 IP 地址的地理位置。这与我家的实际地址并不准确,实际上离我家有好几英里远,但世界上的大致位置相当准确。我们可以用这个分辨率甚至更低的数据做重要的事情。通常,仅仅知道 web 请求的来源国就足以满足许多目的。

Freegeoip lets you make 15000 calls per hour. Each page load counts as one call, and as we will see, each API call also counts as one.

怎么做

我们可以浏览这个页面来获取这些信息,但幸运的是,freegeoip.net 为我们提供了一个方便的 RESTAPI。进一步向下滚动页面,我们可以看到 API 文档:

The freegeoio.net API documentation

我们可以简单地使用 requests 库,使用格式正确的 URL 发出 GET 请求。例如,只需在浏览器中输入以下 URL,即可返回给定 IP 地址的地理编码数据的 JSON 表示形式:

Sample JSON for an IP address

08/01_geocode_address.py中提供了一个 Python 脚本来演示这一点。该方法很简单,包括以下内容:

import json
import requests

raw_json = requests.get("http://www.freegeoip.net/json/63.153.113.92").text
parsed = json.loads(raw_json)
print(json.dumps(parsed, indent=4, sort_keys=True))

它具有以下输出:

{
    "city": "Deer Lodge",
    "country_code": "US",
    "country_name": "United States",
    "ip": "63.153.113.92",
    "latitude": 46.3797,
    "longitude": -112.7202,
    "metro_code": 754,
    "region_code": "MT",
    "region_name": "Montana",
    "time_zone": "America/Denver",
    "zip_code": "59722"
}

Note that your output for this IP address may vary, and surely will with different IP addresses.

如何收集维基百科编辑的 IP 地址

处理地理编码 IP 地址的聚合结果可以提供有价值的见解。这在服务器日志中非常常见,也可用于许多其他情况。许多网站包括内容贡献者的 IP 地址。维基百科提供了他们所有页面的更改历史。由非维基百科注册用户创建的编辑将其 IP 地址发布在历史记录中。我们将研究如何创建一个 scraper 来导航给定维基百科主题的历史,并收集未注册编辑的 IP 地址。

准备

我们将检查对维基百科中的网页抓取页面所做的编辑。该页面位于:https://en.wikipedia.org/wiki/Web_scraping 。以下显示了本页的一小部分:

The view history tab

注意右上角的“查看历史记录”。单击该链接可访问编辑的历史记录:

Inspecting an IP address

我向下滚动了一下,突出显示了匿名编辑。请注意,我们可以使用源代码中的mw-userling mw-anonuserlink类来识别这些匿名编辑条目。

还请注意,您可以指定每页要列出的编辑次数,这可以通过向 URL 添加参数来指定。以下 URL 将为我们提供 500 个最新编辑:

https://en.wikipedia.org/w/index.php?title=Web_scraping &偏移量=&限值=500&动作=历史

因此,我们不需要爬行许多不同的页面,一次遍历 50 个页面,我们只需要做 500 个页面中的一个页面。

怎么做

我们按照以下方法进行配方:

  1. 执行刮取的代码在脚本文件08/02_geocode_wikipedia_edits.py中。运行脚本将生成以下输出(截断为前几个地理 IP):
Reading page: https://en.wikipedia.org/w/index.php?title=Web_scraping&offset=&limit=500&action=history
Got 106 ip addresses
{'ip': '2601:647:4a04:86d0:1cdf:8f8a:5ca5:76a0', 'country_code': 'US', 'country_name': 'United States', 'region_code': 'CA', 'region_name': 'California', 'city': 'Sunnyvale', 'zip_code': '94085', 'time_zone': 'America/Los_Angeles', 'latitude': 37.3887, 'longitude': -122.0188, 'metro_code': 807}
{'ip': '194.171.56.13', 'country_code': 'NL', 'country_name': 'Netherlands', 'region_code': '', 'region_name': '', 'city': '', 'zip_code': '', 'time_zone': 'Europe/Amsterdam', 'latitude': 52.3824, 'longitude': 4.8995, 'metro_code': 0}
{'ip': '109.70.55.226', 'country_code': 'DK', 'country_name': 'Denmark', 'region_code': '85', 'region_name': 'Zealand', 'city': 'Roskilde', 'zip_code': '4000', 'time_zone': 'Europe/Copenhagen', 'latitude': 55.6415, 'longitude': 12.0803, 'metro_code': 0}
{'ip': '101.177.247.131', 'country_code': 'AU', 'country_name': 'Australia', 'region_code': 'TAS', 'region_name': 'Tasmania', 'city': 'Lenah Valley', 'zip_code': '7008', 'time_zone': 'Australia/Hobart', 'latitude': -42.8715, 'longitude': 147.2751, 'metro_code': 0}

该脚本还将 geo IP 写入geo_ips.json文件。下一个配方将使用此文件,而不是再次发出所有页面请求。

工作原理

解释如下。脚本首先执行以下代码:

if __name__ == "__main__":
    geo_ips = collect_geo_ips('Web_scraping', 500)
    for geo_ip in geo_ips:
        print(geo_ip)
    with open('geo_ips.json', 'w') as outfile:
        json.dump(geo_ips, outfile)

调用collect_geo_ips将请求包含指定主题和最多 500 次编辑的页面。然后将这些地理 IP 打印到控制台,并写入geo_ips.json文件。

collect_geo_ips的代码如下:

def collect_geo_ips(article_title, limit):
    ip_addresses = get_history_ips(article_title, limit)
    print("Got %s ip addresses" % len(ip_addresses))
    geo_ips = get_geo_ips(ip_addresses)
    return geo_ips

此函数首先调用get_history_ips,报告发现的数量,然后针对每个 IP 地址向get_geo_ips重复请求。

get_history_ips的代码如下:

def get_history_ips(article_title, limit):
    history_page_url = "https://en.wikipedia.org/w/index.php?title=%s&offset=&limit=%s&action=history" % (article_title, limit)
    print("Reading page: " + history_page_url)
    html = requests.get(history_page_url).text
    soup = BeautifulSoup(html, "lxml")

    anon_ip_anchors = soup.findAll("a", {"class": "mw-anonuserlink"})
    addresses = set()
    for ip in anon_ip_anchors:
        addresses.add(ip.get_text())
    return addresses

这将为历史页面指定 URL,检索页面,然后使用mw-anonuserlink类提取所有不同的 IP 地址。

get_geo_ips然后获取这组 IP 地址,并对每个 IP 地址调用freegeoip.net以获取数据。

def get_geo_ips(ip_addresses):
    geo_ips = []
    for ip in ip_addresses:
        raw_json = requests.get("http://www.freegeoip.net/json/%s" % ip).text
        parsed = json.loads(raw_json)
        geo_ips.append(parsed)
    return geo_ips

还有更多。。。

虽然这些数据很有用,但在我们的下一个配方中,我们将读取写入geo_ips.json(使用熊猫)的数据,并使用条形图可视化用户在各个国家的分布情况。

在维基百科上可视化贡献者位置频率

我们可以利用收集到的数据来确定编辑世界各国维基百科文章的频率。这可以通过按国家对捕获的数据进行分组并计算与每个国家相关的编辑次数来实现。然后,我们将对数据进行排序,并创建条形图以查看结果。

怎么做

这是一个非常简单的任务来执行与熊猫。示例的代码在08/03_visualize_wikipedia_edits.py中。

  1. 代码以导入熊猫和matplotlib.pyplot开始:
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
  1. 我们在上一个配方中创建的数据文件已经是熊猫可以直接读取的格式。这是使用 JSON 作为数据格式的好处之一;pandas 内置了对从 JSON 读取和写入数据的支持。下面使用pd.read_json()功能读取数据,并在控制台上显示前五行:
>>> df = pd.read_json("geo_ips.json")
>>> df[:5])

city country_code country_name ip latitude \
0 Hanoi VN Vietnam 118.70.248.17 21.0333 
1 Roskilde DK Denmark 109.70.55.226 55.6415 
2 Hyderabad IN India 203.217.144.211 17.3753 
3 Prague CZ Czechia 84.42.187.252 50.0833 
4 US United States 99.124.83.153 37.7510

longitude metro_code region_code region_name time_zone \
0 105.8500 0 HN Thanh Pho Ha Noi Asia/Ho_Chi_Minh 
1 12.0803 0 85 Zealand Europe/Copenhagen 
2 78.4744 0 TG Telangana Asia/Kolkata 
3 14.4667 0 10 Hlavni mesto Praha Europe/Prague 
4 -97.8220 0
zip_code 
0 
1 4000 
2 
3 130 00 
4
  1. 出于直接目的,我们只需要country_code列,我们可以使用以下内容提取该列(并显示该结果的前五行):
>>> countries_only = df.country_code
>>> countries_only[:5]

0 VN
1 DK
2 IN
3 CZ
4 US
Name: country_code, dtype:object
  1. 现在我们可以使用.groupby('country_code')对本系列中的行进行分组,在结果中,call .count()将返回每个组中的项数。代码还通过calling .sort_values()将结果从最大值排序到最小值:
>>> counts = df.groupby('country_code').country_code.count().sort_values(ascending=False)
>>> counts[:5]

country_code
US 28
IN 12
BR 7
NL 7
RO 6
Name: country_code, dtype: int64

我们可以从这些结果中看出,美国在编辑方面绝对领先,印度排名第二。

该数据可以很容易地可视化为条形图:

counts.plot(kind='bar')
plt.show()

这导致以下条形图显示了所有国家的总体分布:

Histogram of the edit frequencies

从 StackOverflow 作业列表创建 word cloud

现在让我们来看看如何创建词云。词云是一个图像,它显示了一组文本中关键词的频率。图像中的词越大,它在文本正文中的意义就越明显

准备

我们将使用 Word Cloud 库创建我们的 Word Cloud。图书馆的资料来源可在上找到 https://github.com/amueller/word_cloud 。可以使用pip install wordcloud将此库安装到您的 Python 环境中

怎么做

创建单词 cloud 的脚本在08/04_so_word_cloud.py文件中。该配方继续第 7 章的堆栈溢出配方,以提供数据的可视化

  1. 首先从 NLTK 导入单词 cloud 和频率分布函数:
from wordcloud import WordCloud
from nltk.probability import FreqDist
  1. 然后根据我们从工作列表中收集的单词的概率分布生成单词云:
freq_dist = FreqDist(cleaned)
wordcloud = WordCloud(width=1200, height=800).generate_from_frequencies(freq_dist)

现在我们只需要显示单词 cloud:

import matplotlib.pyplot as plt
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

由此产生的单词 cloud 如下所示:

The word cloud for the job listing

位置和大小有一些内在的随机性,因此得到的结果可能不同。

在维基百科上爬行链接

在本食谱中,我们将编写一个小程序,利用爬网功能,在多个深度级别上搜索维基百科页面上的链接。在此爬网过程中,我们将收集页面与每个页面引用的页面之间的关系。在此期间,我们将在这些页面之间建立一种关系,我们将在下一个配方中最终可视化这些页面。

准备

本例代码在08/05_wikipedia_scrapy.py中。它引用了代码示例的modules/wikipedia文件夹中模块中的代码,因此请确保它位于 Python 路径中。

怎么做

您可以使用示例 Python 脚本。它将使用 Scrapy 抓取单个 Wikipedia 页面。它将抓取的页面是位于的 Python 页面 https://en.wikipedia.org/wiki/Python_(编程语言),并在该页面上收集相关链接

运行时,您将看到与以下类似的输出:

/Users/michaelheydt/anaconda/bin/python3.6 /Users/michaelheydt/Dropbox/Packt/Books/PyWebScrCookbook/code/py/08/05_wikipedia_scrapy.py
parsing: https://en.wikipedia.org/wiki/Python_(programming_language)
parsing: https://en.wikipedia.org/wiki/C_(programming_language)
parsing: https://en.wikipedia.org/wiki/Object-oriented_programming
parsing: https://en.wikipedia.org/wiki/Ruby_(programming_language)
parsing: https://en.wikipedia.org/wiki/Go_(programming_language)
parsing: https://en.wikipedia.org/wiki/Java_(programming_language)
------------------------------------------------------------
0 Python_(programming_language) C_(programming_language)
0 Python_(programming_language) Java_(programming_language)
0 Python_(programming_language) Go_(programming_language)
0 Python_(programming_language) Ruby_(programming_language)
0 Python_(programming_language) Object-oriented_programming

输出的第一部分来自 Scrapy crawler,显示传递给解析方法的页面。这些页面从初始页面开始,通过该页面的前五个最常见链接。

此输出的第二部分表示已爬网的页面以及在该页面上找到的供将来处理的链接。第一个数字是找到关系的爬网级别,然后是父页面和在该页面上找到的链接。对于找到的每个页面/链接,有一个单独的条目。由于这是一个单深度爬网,我们只显示从初始页面找到的页面。

工作原理

让我们从主脚本文件08/05_wikipedia_scrapy.py中的代码开始。首先创建WikipediaSpider对象并运行爬网:

process = CrawlerProcess({
    'LOG_LEVEL': 'ERROR',
    'DEPTH_LIMIT': 1
})

process.crawl(WikipediaSpider)
spider = next(iter(process.crawlers)).spider
process.start()

这告诉 Scrapy,我们希望在一个深度级别上运行它,并且我们得到了一个爬虫的实例,因为我们希望检查它的属性,这些属性是爬虫的结果。然后,将结果打印为以下内容:

print("-"*60)

for pm in spider.linked_pages:
    print(pm.depth, pm.title, pm.child_title)

爬虫程序的每个结果都存储在linked_pages属性中。每个对象都由多个属性表示,包括页面标题(wikipedia URL 的最后一部分)和该页面 HTML 内容中找到的每个页面的标题。

现在让我们来看看爬虫是如何工作的。蜘蛛的代码在modules/wikipedia/spiders.py中。爬虫程序首先定义一个子类 ScrapySpider:

class WikipediaSpider(Spider):
    name = "wikipedia"
    start_urls = [ "https://en.wikipedia.org/wiki/Python_(programming_language)" ]

我们从 Wikipedia 的 Python 页面开始。接下来是几个类级变量的定义,用于定义爬网的操作方式和检索结果:

page_map = {}
linked_pages = []
max_items_per_page = 5
max_crawl_depth = 1

此爬网的每个页面都将由爬行器的解析方法进行处理。让我们浏览一下。它从以下内容开始:

def parse(self, response):
    print("parsing: " + response.url)

    links = response.xpath("//*/a[starts-with(@href, '/wiki/')]/@href")

    link_counter = {}

在每一个维基百科页面中,我们寻找链接从什么开始,在页面中有其他链接,但是这些是这个爬行将被认为重要的。

这个爬虫实现了一个算法,在这个算法中,页面上所有找到的链接都被计算为相似性。有相当多的重复链接。其中一些是虚假的。另一些则代表了多次链接到其他页面的真正重要性。

max_items_per_page定义了我们将进一步调查的当前页面上的链接数量。每个页面上都会有相当多的链接,该算法统计所有类似的链接并将它们放入桶中。然后,它会跟踪max_items_per_page最流行的链接。

此过程通过使用links_counter变量进行管理。这是当前页面和页面上找到的所有链接之间映射的字典。对于我们决定跟随的每个链接,我们计算其在页面上被引用的次数。此变量是该 URL 和计算引用次数的以下对象实例之间的映射:

class LinkReferenceCount:
    def __init__(self, link):
        self.link = link
        self.count = 0

然后,代码将遍历所有已识别的链接:

for l in links:
    link = l.root
    if ":" not in link and "International" not in link and link != self.start_urls[0]:
        if link not in link_counter:
            link_counter[link] = LinkReferenceCount(link)
        link_counter[link].count += 1

这将检查每个链接,并仅根据规定的规则(链接中没有“:”或“国际”,因为它非常流行,所以我们将其排除在外,最后我们不包括起始 URL)。如果链接通过此项,则会创建一个新的LinkReferenceCounter对象(如果以前未看到此链接),或其引用计数增加。

由于每个页面都有可能出现重复链接,所以我们只考虑最常见的链接。

references = list(link_counter.values())
s = sorted(references, key=lambda x: x.count, reverse=True)
top = s[:self.max_items_per_page]

从link_counter字典中,我们提取所有LinkReferenceCounter对象并按计数对它们进行排序,然后选择最上面的max_items_per_page项

下一步是针对这些符合条件的项目,我们将它们记录在类的linked_pages字段中。此列表中的每个对象都是PageToPageMap类型。此类具有以下定义:

class PageToPageMap:
    def __init__(self, link, child_link, depth): #, parent):
        self.link = link
        self.child_link = child_link
        self.title = self.get_page_title(self.link)
        self.child_title = self.get_page_title(self.child_link)
        self.depth = depth

    def get_page_title(self, link):
        parts = link.split("/")
        last = parts[len(parts)-1]
        label = urllib.parse.unquote(last)
        return label

从根本上说,此对象表示链接页面 URL 的源页面 URL,它还跟踪当前的爬网级别。标题属性是维基百科 URL 最后一部分的 URL 解码形式,表示更人性化的 URL 版本。

最后,代码会让人抓取新的页面

for item in top:
    new_request = Request("https://en.wikipedia.org" + item.link,
                          callback=self.parse, meta={ "parent": pm })
    yield new_request

还有更多。。。

此爬网器/算法还跟踪爬网中的深度的当前级别。如果认为新链接超出了爬网的最大深度。虽然这可以通过 Scrapy 控制到某个点,但此代码仍然需要不包含超出最大深度的链接。

这是通过使用PageToPageMap对象的深度字段来控制的。对于爬网的每个页面,我们检查响应是否有元数据,该属性表示给定页面的"parent"PageToPageMap 对象。我们通过以下代码发现:

depth = 0
if "parent" in response.meta:
    parent = response.meta["parent"]
    depth = parent.depth + 1

页面解析器中的代码查看是否存在父对象。只有爬网的第一个页面没有父页面。如果有实例,则认为此爬网的深度更高。创建新的PageToPageMap对象时,此值将传递给它并存储。

代码使用请求对象的元属性将此对象传递到下一级爬网:

meta={ "parent": pm }

通过这种方式,我们可以将数据从一个爬行的爬行层传递到下一个爬行层。

在维基百科上可视化页面关系

在本配方中,我们使用上一配方中收集的数据,并使用 NetworkX Python 库创建页面关系的强制网络可视化。

准备

NetworkX 是用于建模、可视化和分析复杂网络关系的软件。您可以在以下网址找到更多信息:https://networkx.github.io 。可以使用pip install networkx将其安装在 Python 环境中

怎么做

本例的脚本位于08/06_visualizze_wikipedia_links.py文件中。运行时,它会生成 Wikipedia 初始 Python 页面上的链接图:

Graph of the links

现在我们可以看到页面之间的关系了!

工作原理

爬网从定义一级深度爬网开始:

crawl_depth = 1
process = CrawlerProcess({
    'LOG_LEVEL': 'ERROR',
    'DEPTH_LIMIT': crawl_depth
})
process.crawl(WikipediaSpider)
spider = next(iter(process.crawlers)).spider
spider.max_items_per_page = 5
spider.max_crawl_depth = crawl_depth
process.start()

for pm in spider.linked_pages:
    print(pm.depth, pm.link, pm.child_link)
print("-"*80)

此信息类似于上一个配方,我们需要将其转换为 NetworkX 可以用于图形的模型。这从创建 NetworkX 图形模型开始:

g = nx.Graph()

NetworkX 图形由节点和边组成。根据收集的数据,我们必须装箱一组唯一的节点(页面)和边(一个页面引用另一个页面的事实)。这通过以下方式执行:

nodes = {}
edges = {}

for pm in spider.linked_pages:
    if pm.title not in nodes:
        nodes[pm.title] = pm
        g.add_node(pm.title)

    if pm.child_title not in nodes:
        g.add_node(pm.child_title)

    link_key = pm.title + " ==> " + pm.child_title
    if link_key not in edges:
        edges[link_key] = link_key
        g.add_edge(pm.title, pm.child_title)

这将遍历 out crawl 的所有结果,并识别所有唯一的节点(不同的页面),然后识别任何页面和其他页面之间的所有链接。对于每个节点和边缘,我们向 NetworkX 注册这些节点和边缘。

接下来,我们使用 Matplotlib 创建绘图,并告诉 NetworkX 如何在绘图中创建视觉效果:

plt.figure(figsize=(10,8))

node_positions = nx.spring_layout(g)

nx.draw_networkx_nodes(g, node_positions, g.nodes, node_color='green', node_size=50)
nx.draw_networkx_edges(g, node_positions)

labels = { node: node for node in g.nodes() }
nx.draw_networkx_labels(g, node_positions, labels, font_size=9.5)

plt.show()

这其中的重要部分首先是使用 NetworkX 在节点上形成 spring 布局。该布局计算节点的实际位置,但不渲染节点或边。这就是下两行的目的,这两行为 NetworkX 提供了有关如何渲染节点和边的说明。最后,我们需要在节点上添加标签。

还有更多。。。

此爬网只进行了一次深度爬网。通过对代码进行以下更改,可以增加爬网:

crawl_depth = 2
process = CrawlerProcess({
    'LOG_LEVEL': 'ERROR',
    'DEPTH_LIMIT': crawl_depth
})
process.crawl(WikipediaSpider)
spider = next(iter(process.crawlers)).spider
spider.max_items_per_page = 5
spider.max_crawl_depth = crawl_depth
process.start()

从根本上说,唯一的改变是将深度增加一个级别。这将导致以下图形(在任何 spring 图形中都有随机化,因此实际结果具有不同的布局):

Spider graph of the links

这开始变得有趣,因为我们现在开始看到页面之间的相互关系和循环关系。

我敢说你要进一步增加深度和每页的链接数量。

计算分离度

现在,让我们计算任意两个页面之间的分离度。这回答了我们需要从一个源页面穿过多少页面才能找到另一个页面的问题。这可能是一个不平凡的图形遍历问题,因为两个页面之间可能有多条路径。幸运的是,NetworkX,使用完全相同的图形模型,已内置函数以使用上一配方中完全相同的模型解决此问题。

怎么做

本例的脚本位于08/07_degrees_of_separation.py中。该代码与上一个配方相同,具有两个深度的爬网,只是它省略了图形,并要求 NetworkX 解决Python_(programming_language)和Dennis_Ritchie之间的分离度:

Degrees of separation: 1
 Python_(programming_language)
   C_(programming_language)
    Dennis_Ritchie

这告诉我们,从Python_(programming_language)到Dennis_Ritchie我们必须通过另一页:C_(programming_language),因此,一个分离度。如果我们直接去C_(programming_language), 那将是 0 度的分离。

工作原理

这个问题的解决方案是通过一种称为A的算法来解决的。A算法确定图形中两个节点之间的最短路径。请注意,此路径可以是不同长度的多条路径,正确的结果是最短路径。对我们来说,一件好事是 NetworkX 有一个内置函数为我们完成此操作。只需一条简单语句即可:

path = nx.astar_path(g, "Python_(programming_language)", "Dennis_Ritchie")

由此,我们报告了实际路径:

degrees_of_separation = int((len(path) - 1) / 2)
print("Degrees of separation: {}".format(degrees_of_separation))
for i in range(0, len(path)):
    print(" " * i, path[i])

还有更多。。。

有关A*算法的更多信息,请访问查看此页面 https://en.wikipedia.org/A*_ 搜索算法。