待更新

安装BeautifulSoup4

Linux

$sudo apt-get install python-bs4

macOS

$sudo easy_install pip
$pip3 install beautifulsoup4

导入

from bs4 import BeautifulSoup

尝试运行

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page1.html")
bsObj = BeautifulSoup(html.read())
print(bsObj.body.h1)

其中的bsObj可以通过标签来指定获取HTML代码中的一段。

简单异常处理

爬虫运行中可能会遇到各种异常,需要相应做好异常处理。

HTTPError

网页在服务器上不存在时触发,触发时,urlopen函数会抛出HTTPError异常。

try:
    html = urlopen("http://www.pythonscraping.com/pages/page1.html")
except HTTPError as e:
    print(e)
else:
    #程序继续

AttributeError

当调用一个标签时,增加一个检查条件保证标签确实存在是不错的做法。如果调用的标签不存在,BeautifulSoup会返回None对象。如果程序调用了None的子标签,就会发生AttributeError

try:
    badContent = bsObj.body.h1
except AttributeError as e:
    print("Tag was not found.")
else:
    if badContent == None:
        print("Tag was not found.")
    else:
        printf(badContent)

整理

from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from bs4 import BeautifulSoup

def getTitle(url):
    try:
        html = urlopen(url)
    except (HTTPError, URLError) as e:
        return None
    try:
        bsObj = BeautifulSoup(html.read())
        title = bsObj.body.h1
    except AttributeError as e:
        return None
    return title

title = getTitle(input()) #输入网址
if title = None:
    print("Title could not be found")
else:
    print(title)

从网页中摘取需要的数据

不应当简单粗暴地针对网页来取用数据。

使用CSS来获取信息

可以抓出整个页面,然后创建一个BeautifulSoup对象。
通过BeautifulSoup对象,用findAll方法抽取信息。
示例网页

from urllib.request import urlopen
from urllib.error import HTTPError, URLError
from bs4 import BeautifulSoup

html = urlopen("http://www.pythonscraping.com/pages/warandpeace.html")
bsObj = BeautifulSoup(html)
nameList = bsObj.findAll("span", {"class":"green"})
for name in nameList:
    print(name.get_text())

findfindAll方法

findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)
很多时候我们只会用到前两个参数:tagattributes
tag表示需要取得的标签名称,也可以传入多个标签名组成的列表。
attributes是用一个Python字典封装一个标签的若干属性。
recursive是递归参数,如果设置为True,函数就会查找子标签的子标签,一直向下查找。
text是文本参数,用标签的文本内容去匹配。
limit是范围限制参数。
keyword是关键词参数,可以选择有指定属性的标签。这个参数在一些场景中很有用,但是这个功能是完全多余的。
例如这种情况,两行代码完全相同

bsObj.findAll(id = "text")
bsObj.findAll("", {"id": "text"})

而且keyword偶尔也会出现问题,尤其是用class属性查找标签的时候。

导航树

子标签和后代标签

子标签表示下一级,后代标签表示下面左右级别的标签。
一般情况下,BeautifulSoup函数总是处理当前标签的后代标签。例如,bsObj.body.h1选择了body标签后代里的第一个h1标签,不会去找body外面的标签,bsObj.div.findAll(“img”)会找出文档中第一个div标签,然后回去后代里所有的img标签列表。
如果只想找子标签,可以用.children

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)
for child in bsObj.find("table", {"id":"giftList"}).children:
    print(child)

兄弟标签

BeautifulSoup中的next_siblings()函数可以让收集表格数成为简单的事情,尤其是处理带标题行的表格:

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)

for sibling in bsObj.find("table",{"id":"giftList"}).tr.next_siblings:
    print(sibling)

父标签

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)
print(bsObj.find("img",
                 {"src":"../img/gifts/img1.jpg"}
                 ).parent.previous_sibling.get_text())

这段代码找到当前图片的父标签的前一个兄弟标签,获取价格。

获取属性

myTag.attrs可以获取所有属性。
myTag.attrs[“src”]可以获取src属性。

爬取Wiki网址

词条关联

用一个函数爬取指定Wiki网页中内联的其他Wiki词条。

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

def getlinks(articleUrl,):
    html = urlopen("https://en.wikipedia.org"+articleUrl)
    bsObj = BeautifulSoup(html)
    return bsObj.find("div", {"id":"bodyContent"}).findAll("a",href=re.compile("^(/wiki/)((?![:%]).)*S"))

全面爬取去重

爬取网址中的所有链接,用一个集合来去重。

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

urls = set()
def getlinks(articleUrl):
    global urls
    html = urlopen("https://en.wikipedia.org"+articleUrl)
    bsObj = BeautifulSoup(html)
    for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
        if 'href' in link.attrs:
            if link.attrs['href'] not in urls:
                newUrl = link.attrs['href']
                print(newUrl)
                urls.add(newUrl)
                getlinks(newUrl)

采集页面信息

获取页面里面的标题和一些信息。

urls = set()
def getlinks(articleUrl):
    global urls
    html = urlopen("https://en.wikipedia.org"+articleUrl)
    bsObj = BeautifulSoup(html)
    try:
        print(bsObj.h1.get_text())
        print(bsObj.find(id="mw-content-text").findAll("p")[0])
        print(bsObj.find(id="ca-edit").find("span").find("a").attrs['href'])
    except AttributeError:
        print("Some Informations are lost.")

    for link in bsObj.findAll("a",href=re.compile("^(/wiki/)")):
        if 'href' in link.attrs:
            if link.attrs['href'] not in urls:
                newUrl = link.attrs['href']
                print("----------------\n"+newUrl)
                urls.add(newUrl)
                getlinks(newUrl)

跳跃爬虫

这个爬虫可以顺着链接从一个链接跳到另一个,跟着外链跳转。但不同网站的布局截然不同。这就意味着寻找信息和查找方式上都必须极具灵活性。
但是因为网站环境复杂,所以需要包含完整的检查和异常处理。
例如:如果爬虫遇到网站中一个外链都没有,这是程序就会一直在这个网站中运行跳不出去,知道递归爆栈位置。

示例

from urllib.request import urlopen
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import re
import datetime
import random

urls = set()
random.seed(datetime.datetime.now())

#获取页面所有内链
def getInternalLinks(bsObj, includeUrl):
    includeUrl = urlparse(includeUrl).scheme+"://"+urlparse(includeUrl).netloc
    internalLinks = []

    for link in bsObj.findAll("a",href=re.compile("^(/|.*"+includeUrl+")")):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in internalLinks:
                if (link.attrs['href'].startswith('/')):
                    internalLinks.append(includeUrl+link.attrs['href'])
                else:
                    internalLinks.append(link.attrs['herf'])

    return internalLinks

#获取页面所有外链
def getExternalLinks(bsObj, excludeUrl):
    externalLinks = []
    #找出所有以'http'或'www'开头并且不包含当前URL的链接
    for link in bsObj.findAll("a", href=re.compile("^(http|www)((?!"+excludeUrl+").)*$")):
        if link.attrs['href'] is not None:
            if link.attrs['href'] not in externalLinks:
                externalLinks.append(link.attrs['href'])

    return externalLinks

def getRandomExternalLink(startPage):
    html = urlopen(startPage)
    bsObj = BeautifulSoup(html)
    externalLinks = getExternalLinks(bsObj, urlparse(startPage).netloc)
    if len(externalLinks) == 0:
        print("No external links, looking around the site for one")
        domain = urlparse(startPage).scheme+"://"+urlparse(startPage).netloc
        internalLinks = getInternalLinks(bsObj, domain)
        return getRandomExternalLink(
            internalLinks[random.randint(
                    0, len(internalLinks)-1)])
    else:
        return externalLinks[random.randint(0, len(externalLinks)-1)]

def followExternalOnly(startingSite):
    externalLink = getRandomExternalLink(startingSite)
    print("Random external link is: "+externalLink)
    followExternalOnly(externalLink)

followExternalOnly(input())

改进

把任务分解成像“获取页面上所有外链”这样的小函数是不错的做法,以后可以方便地修改代码以满足另一个采集任务的需求。例如,如果我们的目标是采集一个网站所有的外链,并且记录每一个外链,我们可以增加下面的函数:

#收集网站上所有发现的外链
allExtLinks = set()
allIntLinks = set()
def getAllExternalLinks(siteUrl):
    html = urlopen(siteUrl)
    bsObj = BeautifulSoup(html)
    internalLinks = getInternalLinks(bsObj, splitAddress(siteUrl)[0])
    externalLinks = getExternalLinks(bsObj, splitAddress(siteUrl)[0])
    for link in externalLinks:
        if link not in allExtLinks:
            allExtLinks.add(link)
            print(link)
    for link in internalLinks:
        if link not in allIntLinks:
            print("This Link's URL is: "+link)
            allIntLinks.add(link)
            getAllExternalLinks(link)
getInternalLinks(input())

Tips:最好在写爬虫之前写一下提纲,以免自己看不懂自己写的爬虫

用Scrapy采集

写网络爬虫意味着你需要经常不断地重复一些简单任务:找出页面上的所有链接、区分内链与外链、跳转到新的页面。这些基本模式一定要掌握,有几个工具可以帮助自动处理这些细节。
Scrapy就是一个能够大幅度降低网页链接查找和识别工作复杂度的Python库,它还可以让你轻松地采集一个或多个域名的信息。
目前Scrapy已经支持Python3了。
macOS只要在控制台键入pip3 install Scrapy即可安装。
Win平台参考blog Scrapy Python3 安装

创建Scrapy项目

$ scrapy startproject wikiSpider //新建项目wikiSpider
创建之后,会在当前目录生成一个wikiSpider文件夹,在wikiSpider/wikiSpider/spiders/中新建一个articleSpider.py文件。
items.py中定义一个Article类。

# wikiSpider/items.py
from scrapy import Item, Field
class Article(Item):
    title = Field()

articleSpider.py中的类名和爬虫包的名称是不同的,这个类只是目录中的一员,仅仅用于维基词条页面采集,对于一些信息类型较多的大网站,可以为每种不同的信息撤职独立的Scrapy条目。每个条目有不同的子段,但在一个Scrapy项目中运行。

# wikiSpider/spiders/articleSpider.py
from scrapy.selector import Selector
from scrapy import Spider
from wikiSpider.items import Article
class ArticleSpider(Spider):
    name = "article"
    allowed_domains = ["en.wikipedia.org"]
    start_urls = ["http://enwikipedia.org/wiki/Main_Page",
                  "http://enwikipedia.org/wiki/Python_%28programming_language%29"]

    def parse(self, response):
        item = Article()
        title = response.xpath('//h1/text()')[0].extract()
        item['title'] = title
        return item

运行Scrapy项目

$ scrapy crawl article
这条命令会用条目名称acrticle来调用爬虫,
这个爬虫会先进入start_urls里面的两个页面,收集信息,然后停止。虽然这个爬虫很简单,但是如果你有很多URL需要采集,那Scrapy这种用法会很适合。为了让爬虫更完善,需要定义一些规则来让Scrapy可以在每个页面查找URL链接。