我一直在寻找有关如何使用Beautiful Soup将数据轻松地从Wikipedia抓取到CSV文件的答案。到目前为止,这是代码。有更简单的方法吗?

import urllib.request
from bs4 import BeautifulSoup
import csv


#the websites
urls = ['https://en.wikipedia.org/wiki/Transistor_count']
data =[]

#getting the websites and the data
for url in urls:   
##    my_url = requests.get(url)
    my_url = urllib.request.urlopen(url)

    html = my_url.read()
    soup = BeautifulSoup(html,'html.parser')

    My_table = soup.find('table',{'class':'wikitable sortable'})
    My_second_table = My_table.find_next_sibling('table') 

    with open('data.csv', 'w',encoding='UTF-8', newline='') as f:
        fields = ['Title', 'Year']
        writer = csv.writer(f, delimiter=',')
        writer.writerow(fields)


    with open('data.csv', "a", encoding='UTF-8') as csv_file:
        writer = csv.writer(csv_file, delimiter=',')    
        for tr in My_table.find_all('tr')[2:]: # [2:] is to skip empty and header 
            tds = tr.find_all('td')
            try:
                title = tds[0].text.replace('\n','')
            except:
                title = ""
            try:
                year = tds[2].text.replace('\n','')
            except:
                year = ""

            writer.writerow([title, year])

    with open('data.csv', "a", encoding='UTF-8') as csv_file:
        writer = csv.writer(csv_file, delimiter=',')    
        for tr in My_second_table.find_all('tr')[2:]: # [2:] is to skip empty and header 
            tds = tr.find_all('td')
            row = "{}, {}".format(tds[0].text.replace('\n',''), tds[2].text.replace('\n',''))
            writer.writerow(row.split(','))


评论

@AlexV我被用户建议在此处发布关于stackoverflow的内容。我只在这里发布是因为在stackoverflow上发布之后,他们告诉我要这样做。我的代码正在运行,但不是我想要的方式。但是,该问题已解决,因此,如果您愿意,可以删除此帖子。还是谢谢你。

您还可以在帖子中解决该问题,为该问题改写单词,然后将代码留给实际审核。

@AlexV,谢谢,我改写了它,但是我不知道是否需要它了,但是我认为对您的代码进行审查总是很不错的。

通常,当我要从网页中获取表格时,请用鼠标将其选中,粘贴

stackoverflow.com/questions/41510383/…

#1 楼


有没有一种方法可以简化此代码?


是的。不要抓维基百科。您在“我需要刮擦这东西之前”的第一个想法。应该是“是否有可以向我提供所需数据的API?”在这种情况下,存在超级链接。

有很多信息链接,例如StackOverflow问题,但最后阅读API文档确实是正确的选择。这应该可以帮助您开始:

from pprint import pprint
import requests, wikitextparser

r = requests.get(
    'https://en.wikipedia.org/w/api.php',
    params={
        'action': 'query',
        'titles': 'Transistor_count',
        'prop': 'revisions',
        'rvprop': 'content',
        'format': 'json',
    }
)
r.raise_for_status()
pages = r.json()['query']['pages']
body = next(iter(pages.values()))['revisions'][0]['*']
doc = wikitextparser.parse(body)
print(f'{len(doc.tables)} tables retrieved')

pprint(doc.tables[0].data())


这似乎比抓取页面更重要,但是API访问可以为您提供结构化的数据,从而绕开了您不应该使用的HTML呈现步骤不必处理。此结构化数据是本文的实际来源,并且更可靠。

评论


\ $ \ begingroup \ $
最后,您仍然必须解析Wikitext。对我来说还不清楚为什么这比解析HTML更好/更容易-它们只是两种不同的标记格式。如果Wikipedia具有仅用于从文章中的表中提取数据的实际语义API,那将是不同的。
\ $ \ endgroup \ $
–杰克M
19-09-15在11:04

\ $ \ begingroup \ $
@JackM-给出了容易选择的选择:让MediaWiki呈现为供人类浏览器表示的格式,或者让MediaWiki返回旨在供人类或计算机使用的结构化数据,我会每次都选择后者对于这种应用。并非所有标记格式都相等,因此这里的意图很重要。 MediaWiki完全有可能更改其渲染引擎,仍然返回视觉上有效的Web内容,但会破坏所有对HTML进行假设的抓取工具。
\ $ \ endgroup \ $
– Reinderien
19-09-15在13:53

\ $ \ begingroup \ $
你提出了一些好观点。但是,对于此应用程序(一次性脚本)而言,解析HTML一样好,并且避免了花时间学习新技术和新工具(Wikitext和Wikitext解析库)。基本上,即使大概比OP更有经验,在这种情况下,我可能也会选择在他们的鞋子上做同样的事情,尽管您的一般观点是,在尝试抓取之前应该始终寻找API,这绝对是他们应该学习的课程在船上。
\ $ \ endgroup \ $
–杰克M
19-09-15在13:58

\ $ \ begingroup \ $
@JackM如果页面的UI已更新,则抓取器会损坏,因此,如果将来要在任何时候重新运行该脚本,则该方法将不太有用。另一方面,API通常是版本化的,并且更改的频率往往少于HTML
\ $ \ endgroup \ $
–触摸我的身体
19-09-16在18:22

\ $ \ begingroup \ $
有趣的是,今天遇到了这种情况,而目前正相反。主要是因为API太烂了,我真的不想浪费时间来学习它的1倍...
\ $ \ endgroup \ $
– Peilonrayz
20 May 7'23:03

#2 楼

让我告诉您有关IMPORTHTML() ...



所以这是您在Google表格中需要的所有代码:

=IMPORTHTML("https://en.wikipedia.org/wiki/Transistor_count", "table", 2)


导入似乎可以正常工作:


并且可以将表格下载为CSV:

Processor,Transistor count,Date of introduction,Designer,MOS process,Area
"MP944 (20-bit, *6-chip*)",,1970[14] (declassified 1998),Garrett AiResearch,,
"Intel 4004 (4-bit, 16-pin)","2,250",1971,Intel,"10,000 nm",12 mm²
"Intel 8008 (8-bit, 18-pin)","3,500",1972,Intel,"10,000 nm",14 mm²
"NEC μCOM-4 (4-bit, 42-pin)","2,500[17][18]",1973,NEC,"7,500 nm[19]",*?*
Toshiba TLCS-12 (12-bit),"over 11,000[20]",1973,Toshiba,"6,000 nm",32 mm²
"Intel 4040 (4-bit, 16-pin)","3,000",1974,Intel,"10,000 nm",12 mm²
"Motorola 6800 (8-bit, 40-pin)","4,100",1974,Motorola,"6,000 nm",16 mm²
...
...


您只需要清理数字,就必须使用API​​或使用BeautifulSoup进行抓取。

评论


\ $ \ begingroup \ $
将原始文本复制/粘贴到电子表格中(在我的情况下,将其粘贴到Numbers.app中)会自动自动选择表格格式。做完了
\ $ \ endgroup \ $
–亚历山大
19年9月14日在0:09

\ $ \ begingroup \ $
这真可怕。
\ $ \ endgroup \ $
– Volker Siegel
19-09-15在5:27

\ $ \ begingroup \ $
@VolkerSiegel:看起来Google知道从网站抓取数据的一两件事。 :D
\ $ \ endgroup \ $
–埃里克·杜米尼尔(Eric Duminil)
19-09-15在8:49

#3 楼

确保遵循命名约定。您不恰当地命名了两个变量:

My_table = soup.find('table',{'class':'wikitable sortable'})
My_second_table = My_table.find_next_sibling('table')


这些只是普通变量,而不是类名,因此应小写:

my_table = soup.find('table',{'class':'wikitable sortable'})
my_second_table = my_table.find_next_sibling('table')



可以做两次

try:
    title = tds[0].text.replace('\n','')
except:
    title = ""



我要指定要捕获的确切异常,所以您不要如果将来开始进行更改,则不会意外隐藏“真实”错误。我假设您打算在这里捕获一个AttributeError
因为您本质上有两次相同的代码,并且由于代码庞大,所以我将其分解为自己的函数。

类似的东西:

import bs4

def eliminate_newlines(tag: bs4.element.Tag) -> str:  # Maybe pick a better name
    try:
        return tag.text.replace('\n', '')

    except AttributeError:  # I'm assuming this is what you intend to catch
        return ""


现在with open块更整洁了:

with open('data.csv', "a", encoding='UTF-8') as csv_file:
    writer = csv.writer(csv_file, delimiter=',')    
    for tr in My_table.find_all('tr')[2:]: # [2:] is to skip empty and header 
        tds = tr.find_all('td')

        title = eliminate_newlines(tds[0])
        year = eliminate_newlines(tds[2])

        writer.writerow([title, year])


编辑:我当时正在洗个澡,意识到您实际上可能打算在页面变形或其他问题时使用IndexError。虽然是相同的想法,但将代码移到函数中以减少重复。诸如此类:

from typing import List

def eliminate_newlines(tags: List[bs4.element.Tag], i: int) -> str:
    return tags[i].text.replace('\n', '') if len(tags) < i else ""


也可以使用条件语句而不是表达式来完成。我认为这很简单,所以单行就可以了。


如果您使用的是Python的较新版本,则应使用以下行:

"{}, {}".format(tds[0].text.replace('\n',''), tds[2].text.replace('\n',''))


可以使用f字符串进行就地字符串内插:

f"{tds[0].text.replace('\n', '')}, {tds[2].text.replace('\n', '')}"


在这种情况下,增益为不多它们对于更复杂的格式很有帮助。

#4 楼

使用Wikipedia API

如另一个答案中所述,Wikipedia提供了HTTP API来获取文章内容,您可以使用它以比HTML更干净的格式获取内容。通常这会好得多(例如,对于我写的这个提取Wikipedia文章第一句话的小项目来说,这是一个更好的选择)。

但是,在您的情况下,您必须解析表无论如何。无论是从HTML解析还是从Wikipedia API的“ wikitext”格式解析它们,我都认为没有太大的区别。因此,我认为这是主观的。

使用请求,而不是urllib

请不要使用urllib,除非您处于由于某种原因而无法安装外部库的环境中。 requests库是获取HTML的首选。

要获取HTML,代码应为:

html = requests.get(url).text


import requests顶置。对于您的简单示例,这实际上并没有那么容易,但这只是Python编程中始终使用requests而不是urllib的一般最佳实践。这是首选的库。

所有带块的库都有什么功能?

您不需要三个with块。当我看一下这段代码时,三个with块使代码看起来比实际要复杂得多-似乎您正在编写多个CSV,而实际上并不是。只需使用一个,它的工作原理是一样的:

with open('data.csv', 'w',encoding='UTF-8', newline='') as f:
    fields = ['Title', 'Year']
    writer = csv.writer(f, delimiter=',')
    writer.writerow(fields)

    for tr in My_table.find_all('tr')[2:]: # [2:] is to skip empty and header
        tds = tr.find_all('td')
        try:
            title = tds[0].text.replace('\n','')
        except:
            title = ""
        try:
            year = tds[2].text.replace('\n','')
        except:
            year = ""

        writer.writerow([title, year])

    for tr in My_second_table.find_all('tr')[2:]: # [2:] is to skip empty and header
        tds = tr.find_all('td')
        row = "{}, {}".format(tds[0].text.replace('\n',''), tds[2].text.replace('\n',''))
        writer.writerow(row.split(','))


那两个表真的不同吗?

您有两个for循环,每个循环处理表格并将其写入CSV。两个for循环的主体是不同的,因此乍一看似乎这两个表具有不同的格式等等……但是它们却没有。您可以将第一个for循环的主体复制粘贴到第二个中,并且工作原理相同:

for tr in My_table.find_all('tr')[2:]: # [2:] is to skip empty and header
    tds = tr.find_all('td')
    try:
        title = tds[0].text.replace('\n','')
    except:
        title = ""
    try:
        year = tds[2].text.replace('\n','')
    except:
        year = ""

    writer.writerow([title, year])

for tr in My_second_table.find_all('tr')[2:]: # [2:] is to skip empty and header
    tds = tr.find_all('td')
    try:
        title = tds[0].text.replace('\n','')
    except:
        title = ""
    try:
        year = tds[2].text.replace('\n','')
    except:
        year = ""


使用上面的代码,与生成的CSV的唯一区别是逗号后没有空格,我认为这对您来说并不重要。既然我们已经建立了两个循环中的代码,则不必相同,我们只需执行以下操作:

table_rows = My_table.find_all('tr')[2:] + My_second_table.find_all('tr')[2:]

for tr in table_rows:
    tds = tr.find_all('td')
    try:
        title = tds[0].text.replace('\n','')
    except:
        title = ""
    try:
        year = tds[2].text.replace('\n','')
    except:
        year = ""

    writer.writerow([title, year])


只有一个for循环。更容易理解!

手工解析字符串

,因此根本不需要第二个循环,但是无论如何,让我们看一下其中的代码:

row = "{}, {}".format(tds[0].text.replace('\n',''), tds[2].text.replace('\n',''))
writer.writerow(row.split(','))


嗯...您只是将两个字符串用逗号连接在一起,只是调用split,并在下一行的逗号处将它们分开。我确定现在已经向您指出了这一点,您可以看到它毫无意义,但是我想在这两行代码中将您引向另一件事。

您实际上是在尝试解析数据用row.split手动操作,这总是很危险的。这是有关编程的重要且通用的课程。如果芯片名称中包含逗号怎么办?那么row所包含的逗号将不止于您输入的逗号,并且对writerow的调用最终将插入多于两列!

除非绝对必要,否则请不要手工解析数据,并且除非绝对必要,否则切勿手动以CSV或JSON等格式写入数据。始终使用库,因为芯片名称中总是会出现病理性的极端情况,例如逗号,您不会想到这会破坏代码。如果这些库已经存在了一段时间,则可以解决这些错误。使用这两行代码:

row = "{}, {}".format(tds[0].text.replace('\n',''), tds[2].text.replace('\n',''))
writer.writerow(row.split(','))


您试图手动将表格行分为两列,这就是为什么您犯了一个错误(就像任何人一样)将)。而在第一个循环中,进行拆分的代码是两行:

title = tds[0].text.replace('\n','')
year = tds[2].text.replace('\n','')


在这里,您依靠BeautifulSoup将列干净地分为tds[0]tds[2],这更安全,这就是为什么此代码更好的原因。
输入解析和输出生成的混合

解析HTML的代码与生成CSV的代码混合在一起。这很难将问题分解为子问题。编写CSV的代码应该只考虑标题和年份,而不必知道它们来自HTML,而解析HTML的代码应该只是解决提取标题和年份的问题,它不应该将数据写入CSV。换句话说,我希望写CSV的for循环看起来像这样:

for (title, year) in rows:
    writer.writerow([title, year])


我们可以通过重写with块来做到这一点,如下所示:

with open('data.csv', 'w',encoding='UTF-8', newline='') as f:
    fields = ['Title', 'Year']
    writer = csv.writer(f, delimiter=',')
    writer.writerow(fields)

    table_rows = My_table.find_all('tr')[2:] + My_second_table.find_all('tr')[2:]
    parsed_rows = []
    for tr in table_rows:
        tds = tr.find_all('td')
        try:
            title = tds[0].text.replace('\n','')
        except:
            title = ""
        try:
            year = tds[2].text.replace('\n','')
        except:
            year = ""
        parsed_rows.append((title, year))

    for (title, year) in parsed_rows:
        writer.writerow([title, year])


将其分解为函数

要使代码更具可读性并将HTML内容与CSV内容真正分开,我们可以将脚本分解为功能。这是我完整的脚本。

import requests
from bs4 import BeautifulSoup
import csv

urls = ['https://en.wikipedia.org/wiki/Transistor_count']
data = []

def get_rows(html):
    soup = BeautifulSoup(html,'html.parser')
    My_table = soup.find('table',{'class':'wikitable sortable'})
    My_second_table = My_table.find_next_sibling('table')
    table_rows = My_table.find_all('tr')[2:] + My_second_table.find_all('tr')[2:]
    parsed_rows = []
    for tr in table_rows:
        tds = tr.find_all('td')
        try:
            title = tds[0].text.replace('\n','')
        except:
            title = ""
        try:
            year = tds[2].text.replace('\n','')
        except:
            year = ""
        parsed_rows.append((title, year))

    return parsed_rows

for url in urls:
    html = requests.get(url).text
    parsed_rows = get_rows(html)

    with open('data.csv', 'w',encoding='UTF-8', newline='') as f:
        fields = ['Title', 'Year']
        writer = csv.writer(f, delimiter=',')
        writer.writerow(fields)
        for (title, year) in parsed_rows:
            writer.writerow([title, year])


get_rows可以作为生成器而不是常规函数会更好,但这是高级Python编程。现在很好。

评论


\ $ \ begingroup \ $
“不幸的是,“在Python 3中,请求库是内置的…”。但是您可以使用pip安装请求轻松安装它。
\ $ \ endgroup \ $
– Grooveplex
19-09-15在17:46

\ $ \ begingroup \ $
@grooveplex哦,也许它只是Ubuntu附带的?我认为我不必在最近的内存中安装请求。
\ $ \ endgroup \ $
–杰克M
19 Sep 15 '19:10



#5 楼

聚会晚了一点,但是怎么样? pandas方法返回页面上所有表格的列表作为数据框。请注意,感兴趣的表位于索引位置1。您可能希望对数据进行一些清理,但这应该可以帮助您开始。
import pandas as pd
tables = pd.read_html("https://en.wikipedia.org/wiki/Transistor_count")
tables[1].to_csv('data.csv', index=False)

Processor,MOS transistor count,Date ofintroduction,Designer,MOS process(nm),Area (mm2),Unnamed: 6
"MP944 (20-bit, 6-chip, 28 chips total)","74,442 (5,360 excl. ROM & RAM)[23][24]",1970[21][a],Garrett AiResearch,?,?,
"Intel 4004 (4-bit, 16-pin)",2250,1971,Intel,"10,000 nm",12 mm2,
"TMX 1795 (?-bit, 24-pin)","3,078[25]",1971,Texas Instruments,?,30 mm2,
"Intel 8008 (8-bit, 18-pin)",3500,1972,Intel,"10,000 nm",14 mm2,
"NEC μCOM-4 (4-bit, 42-pin)","2,500[26][27]",1973,NEC,"7,500 nm[28]",?,

文档:pandas.read_csv

#6 楼

当您要求一种更简单的方法时,您是指一种像卡通中那样使用“真实代码”的更简单的方法吗?

看起来很简单,但我认为可能更简单。

通常,当我想要从网页中获取表格时,我会用鼠标选择该表格并将特殊(未格式化)粘贴到电子表格中。值得庆幸的是,我不必与NSA的最大竞争对手(Google)打交道。

评论


\ $ \ begingroup \ $
为什么反对投票?这绝对是最简单的方法来获得单个表而不必大惊小怪。无论如何,都必须清理桌子,这通常需要进行一些手动检查。重复几次后,可以随意进行自动化,但是不要一开始就进行自动化。过早的优化是万恶之源-Knuth。
\ $ \ endgroup \ $
– Contango
20 Jan 1 '20 at 9:18