由于 HTML 的组织方式,这个特定的网站有点困难。包含信息的相关标签并没有太多的区别特征,所以我们必须要聪明一点。更复杂的是,包含整个页面信息的 div 是同级的。我们还必须用一些独创性来弥补这种对网页设计的讽刺。
我确实注意到整个页面(几乎完全)一致的模式。每个“类型”和基础部分分为 3 个 div:
- 包含类型和口袋妖怪的 div,例如
Dark Type: Tyranitar。
- 包含“专业”和动作的 div。
- 包含“评分”和评论的 div。
这里的基本思想是,我们可以通过一个大致如下的过程来开始组织这种标记混乱:
- 识别每个类型的标题 div
- 对于每个 div,通过访问其兄弟来获取其他两个 div
- 从每个 div 中解析信息
考虑到这一点,我提出了一个可行的解决方案。代码的主体由 5 个函数组成。一个用于查找每个部分,一个用于提取兄弟,三个函数用于解析每个 div。
import re
import json
import requests
from pprint import pprint
from bs4 import BeautifulSoup
def type_section(tag):
"""Find the tags that has the move type and pokemon name"""
pattern = r"[A-z]{3,} Type: [A-z]{3,}"
# if all these things are true, it should be the right tag
return all((tag.name == 'div',
len(tag.get('class', '')) == 1,
'field__item' in tag.get('class', []),
re.findall(pattern, tag.text),
))
def parse_type_pokemon(tag):
"""Parse out the move type and pokemon from the tag text"""
s = tag.text.strip()
poke_type, pokemon = s.split(' Type: ')
return {'type': poke_type, 'pokemon': pokemon}
def parse_speciality(tag):
"""Parse the tag containing the speciality and moves"""
table = tag.find('table')
rows = table.find_all('tr')
speciality_row, fast_row, charge_row = rows
speciality_types = []
for anchor in speciality_row.find_all('a'):
# Each type 'badge' has a href with the type name at the end
href = anchor.get('href')
speciality_types.append(href.split('#')[-1])
fast_move = fast_row.find('td').text
charge_move = charge_row.find('td').text
return {'speciality': speciality_types,
'fast_move': fast_move,
'charge_move': charge_move}
def parse_rating(tag):
"""Parse the tag containing categorical ratings and commentary"""
table = tag.find('table')
category_tags = table.find_all('th')
strength_tag, meta_tag, future_tag = category_tags
str_rating = strength_tag.parent.find('td').text.strip()
meta_rating = meta_tag.parent.find('td').text.strip()
future_rating = meta_tag.parent.find('td').text.strip()
blurb_tags = table.find_all('td', {'colspan': '2'})
if blurb_tags:
# `if` to accomodate fire section bug
str_blurb_tag, meta_blurb_tag, future_blurb_tag = blurb_tags
str_blurb = str_blurb_tag.text.strip()
meta_blurb = meta_blurb_tag.text.strip()
future_blurb = future_blurb_tag.text.strip()
else:
str_blurb = None;meta_blurb=None;future_blurb=None
return {'strength': {
'rating': str_rating,
'commentary': str_blurb},
'meta': {
'rating': meta_rating,
'commentary': meta_blurb},
'future': {
'rating': future_rating,
'commentary': future_blurb}
}
def extract_divs(tag):
"""
Get the divs containing the moves/ratings
determined based on sibling position from the type tag
"""
_, speciality_div, _, rating_div, *_ = tag.next_siblings
return speciality_div, rating_div
def main():
"""All together now"""
url = 'https://pokemongo.gamepress.gg/best-attackers-type'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')
types = {}
for type_tag in soup.find_all(type_section):
type_info = {}
type_info.update(parse_type_pokemon(type_tag))
speciality_div, rating_div = extract_divs(type_tag)
type_info.update(parse_speciality(speciality_div))
type_info.update(parse_rating(rating_div))
type_ = type_info.get('type')
types[type_] = type_info
pprint(types) # We did it
with open('pokemon.json', 'w') as outfile:
json.dump(types, outfile)
目前,整个事情只有一个小扳手。还记得我说过这种模式几乎完全一致吗?好吧,Fire 类型在这里是一个奇怪的球,因为它们包含了该类型的两个口袋妖怪,所以Fire 类型的结果不正确。我或一些勇敢的人可能会想出一个办法来解决这个问题。或者他们可能会在未来决定一只火系宝可梦。
可以在this gist 中找到此代码、生成的 json(美化)和使用的 HTML 响应存档。