摘要:这张地图描绘了一些目前最热门的民航线路,每条线路都用不同的颜色和宽度表示出了最近一年有多少乘客往返于这两个机场之间。
数据收集
当我开始收集用于这张地图的数据时,我知道并不是所有的机场都在它的维基百科页面上公布了它和不同目的地之间往返的乘客数目。但我也不确定是否可以根据其他机场给出的乘客数目来填补这些空缺。
莫斯科的两大机场,谢列梅捷沃国际机场(Sheremetyevo)和多莫杰多沃国际机场(Domodedovo)都没有在维基百科中公布在它们和一些热门目的地之间往返的乘客数目。但是曼谷(Bangkok)的素万那普机场(Suvarnabhumi Airport)在维基上公布了2013年它与谢列梅捷沃机场以及多莫杰多沃机场之间往返的乘客数目分别是266,889和316,055人。新西伯利亚(Novosibirsk)的托尔马切沃机场(Tolmachevo)也公布2013年它与素万那普机场之间往返的乘客数目为215,408,这与素万那普机场在维基上公布的数字212,715很接近。
捷克的布拉格机场(Prague Airport)和法国巴黎的戴高乐机场(Charles de Gaulle Airport)分别公布了它们和谢列梅捷沃机场之间往返的乘客数目是637,566和790,922人。但其它有些机场,例如乌克兰基辅(Kiev)的鲍里斯波尔机场(Boryspil)则是将谢列梅捷沃机场(Sheremetyevo)和多莫杰多沃机场(Domodedovo)的数字合并在一起以城市为单位来统计乘客数目。
谢列梅捷沃机场(Sheremetyevo)&埃及的沙姆沙伊赫机场(Sharm el-Sheikh),谢列梅捷沃机场(Sheremetyevo)&俄罗斯的克拉斯诺达尔以及谢列梅捷沃机场(Sheremetyevo)&加里宁格勒机场这三条航线都没有公布相应的乘客数目。同时,中国、印度、巴西和南美的大部分客流也没有按照出发/抵达的机场进行分类统计。
我从28,731个标题中包含“机场”的维基百科条目中提取出5,958个机场,其中343个包含了按照目的地分类的乘客数目信息。这343个机场中的绝大部分至少列出了本年度与其往返最频繁的十大机场以及相应的乘客数目,很多列出了前二十位,有些明星机场(大多在西欧和东南亚地区)列出了50个以上。
实际数字可能更高,但这是我的分析器所能找到的所有结果。
搭建环境
我在我的Ubuntu 14.04系统中装了一些用来收集并展示数据的工具。
$ sudo apt-get update $ sudo apt-get install python-mpltoolkits.basemap \ pandoc \ libxml2-dev \ libxslt1-dev \ redis-server $ sudo pip install docopt
在这个项目的数据收集阶段,我几乎完全是在虚拟环境中工作的。但是当我想通过pip安装Matplotlib中的Basemap时我碰到了一些困难,所以我还是使用Ubuntu系统。
我将绘制地图的任务转移到plot.py中来完成。为了完成我在app.py中做的所有工作,我在虚拟环境中安装了11个软件包:
$ virtualenv passengers $ source passengers/bin/activate $ pip install -r requirements.txt
在完成数据收集,即将进入数据展示阶段时,你可按照以下方法退出虚拟环境:
$ deactivate
下载维基百科中的内容
如果可以不用向远程服务器发送千万条网络请求,即使是通过队列的形式,我将会竭尽全力实现这个目标。因此,我下载了大约11G大小的维基百科中所有英语条目。你可以将其作为一个单独的文件进行下载,也可以分块进行。
$ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles1.xml-p000000010p000010000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles2.xml-p000010002p000025001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles3.xml-p000025001p000055000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles4.xml-p000055002p000104998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles5.xml-p000105002p000184999.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles6.xml-p000185003p000305000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles7.xml-p000305002p000465001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles8.xml-p000465001p000665001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles9.xml-p000665001p000925001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles10.xml-p000925001p001325001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles11.xml-p001325001p001825001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles12.xml-p001825001p002425000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles13.xml-p002425002p003125001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles14.xml-p003125001p003925001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles15.xml-p003925001p004824998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles16.xml-p004825005p006025001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles17.xml-p006025001p007524997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles18.xml-p007525004p009225000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles19.xml-p009225002p011124997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles20.xml-p011125004p013324998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles21.xml-p013325003p015724999.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles22.xml-p015725013p018225000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles23.xml-p018225004p020925000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles24.xml-p020925002p023725001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles25.xml-p023725001p026624997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles26.xml-p026625004p029624976.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles27.xml-p029625017p047137381.bz2
缩减数据大小
我希望能一步就将所有标题含有“机场”的条目的标题和正文都从相应的XML文件中提取出来。这个过程在我的电脑上需要运行一个小时以上,但好处是即使后面的步骤失败了,我也不需要再重复这个步骤了。
$ python app.py get_wikipedia_content title_article_extract.json
这样,约11G的压缩条目文件就变成了只有68 MB的非压缩JSON文件。我很开心Python的标准库几乎可以完成我所需要的所有事情。
import bz2 import codecs from glob import glob from lxml import etree def get_parser(filename): ns_token = '{http://www.mediawiki.org/xml/export-0.10/}ns' title_token = '{http://www.mediawiki.org/xml/export-0.10/}title' revision_token = '{http://www.mediawiki.org/xml/export-0.10/}revision' text_token = '{http://www.mediawiki.org/xml/export-0.10/}text' with bz2.BZ2File(filename, 'r+b') as bz2_file: for event, element in etree.iterparse(bz2_file, events=('end',)): if element.tag.endswith('page'): namespace_tag = element.find(ns_token) if namespace_tag.text == '0': title_tag = element.find(title_token) text_tag = element.find(revision_token).find(text_token) yield title_tag.text, text_tag.text element.clear() def pluck_wikipedia_titles_text(pattern='enwiki-*-pages-articles*.xml-*.bz2', out_file='title_article_extract.json'): with codecs.open(out_file, 'a+b', 'utf8') as out_file: for bz2_filename in sorted(glob(pattern), key=lambda a: int( a.split('articles')[1].split('.')[0]), reverse=True): print bz2_filename parser = get_parser(bz2_filename) for title, text in parser: if 'airport' in title.lower(): out_file.write(json.dumps([title, text], ensure_ascii=False)) out_file.write('\n')
这些文件是按照由大到小的顺序来处理的,因为我希望能及早发现任何内存和循环方面的问题。
转换维基百科文件格式(Markdown变为HTML )
在我最初开始尝试制作这个地图时,我曾经从维基百科下载了少量机场条目的HTML文件,并用Beautiful Soup来分析这些机场的特点以及乘客数目。这个程序很简单并且在我测试的一些实例中也运行地很好。但是我从维基百科下载的文件是它们特有的Markdown格式,我需要将其转化为HTML格式。我试着用creole和pandoc来进行转化,都不能得到统一的结果。这些机场条目文件没有完全被转化成相同的格式,因此,即使数据都在,但是它们可能会以非常不同的形式存在于两个不同的页面上。信息框和表格在转化为HTML时也不能正确显示。有时,表格中的每一小格都会单独显示为一行。
为了能够按时完成任务,我决定尝试三种不同的方法来提取数据:先用creole,如果不行就换用pandoc。如果pandoc也不行,我就手工连接到维基百科网站,下载HTML文件。我不想向维基的服务器发送过多的请求,因此我设置每10秒发送3次,同时将文件暂存在Redis中。
import ratelim import redis import requests @ratelim.greedy(3, 10) # 3 calls / 10 seconds def get_wikipedia_page_from_internet(url_suffix): url = 'https://en.wikipedia.org%s' % url_suffix resp = requests.get(url) assert resp.status_code == 200, (resp, url) return resp.content def get_wikipedia_page(url_suffix): redis_con = redis.StrictRedis() redis_key = 'wikipedia_%s' % sha1(url_suffix).hexdigest()[:6] resp = redis_con.get(redis_key) if resp is not None: return resp html = get_wikipedia_page_from_internet(url_suffix) redis_con.set(redis_key, html) return html
如果某个链接连接失败或者该页面不存在,程序会自动进行下一个:
try: html = get_wikipedia_page(url_key) except (AssertionError, requests.exceptions.ConnectionError): pass # Some pages link to 404s, just move on... else: soup = BeautifulSoup(html, "html5lib")
我发现有些HTML文件非常混乱以至于Beautiful Soup会到达CPython中设置的循环次数极限。如果碰到这种情况,我会直接进行下一条,因为少量缺失数据对我影响并不是很大,不完美并不代表不好。
try: soup = BeautifulSoup(html, "html5lib") passenger_numbers = pluck_passenger_numbers(soup) except RuntimeError as exc: if 'maximum recursion depth exceeded' in exc.message: passenger_numbers = {} else: raise exc
采集机场的各项指标及中转情况的指令如下:
$ python app.py pluck_airport_meta_data \ title_article_extract.json \ stats.json
选择地图样式
我不想用柱状图来呈现这些数据,但是直接在一张主要由蓝色和绿色构成的地图上直接画线也会让我觉得图片背景很嘈杂。
幸运的是,我刚好看到James Cheshire发表的一篇博客。James Cheshire是伦敦大学学院(University College London,UCL)地理系的讲师,他在这篇博客中评论了Michael Markieta绘制的一幅地图。Michael在这副地图中画出了伦敦的四个机场和它们在全世界的各个目的地之间的航线,用作背景的地图是美国宇航局(NASA)制作的灯光夜景图(Night lights map)。
我一直非常努力地寻找一种配色方案可以从视觉上区分这些航线,Michael使用的这种从粉红到大红色的方案看上去不错。我在Color Brewer 2.0中找到了类似的配色。除了颜色之外,我还使用了不同的透明度和线的宽度来表示在某一年度中有多少乘客使用了该航线。
0.3, 250000), ('#ed8d75', 0.5, 0.4, 500000), ('#ef684b', 0.6, 0.6, 1000000), ('#e93a27', 0.7, 0.8, 2000000), ) for iata_pair, passenger_count in pairs.iteritems(): colour, alpha, linewidth, _ = display_params[0] for _colour, _alpha, _linewidth, _threshold in display_params: if _threshold > passenger_count: break colour, alpha, linewidth = _colour, _alpha, _linewidth
绘制地图
接下来很简单,我们可以直接用Basemap软件包来绘制地图。唯一的问题是一些跨越太平洋的航线会终止于图片的边缘,画一条直线到达图片的另一侧,然后一直延伸到目的地。这是画大圆周方法中的一个文件记录类问题。
line, = m.drawgreatcircle(long1, lat1, long2, lat2, linewidth=linewidth, color=colour, alpha=alpha, solid_capstyle='round') p = line.get_path() # Find the index which crosses the dateline (the delta is large) cut_point = np.where(np.abs(np.diff(p.vertices[:, 0])) > 200)[0] if cut_point: cut_point = cut_point[0] # Create new vertices with a nan in between and set # those as the path's vertices new_verts = np.concatenate([p.vertices[:cut_point, :], [[np.nan, np.nan]], p.vertices[cut_point+1:, :]]) p.codes = None p.vertices = new_verts
绘制PNG和SVG地图的命令如下:
# Exit the virtual environment in order to use the system-wide # Basemap package: $ deactivate $ python plot.py render stats.json out.png $ python plot.py render stats.json out.svg
原文发布时间为:2015-12-03