python爬虫入门

简介

爬虫程序主要内容就两部分。
1,html获取器
2,html解析器
入门足够了。管理url、伪装用户行为、运行javascript等属于高级操作,不算入门,俺也没学过。
最后附上环境搭建部分。

以下各小节均以从ICML2021会议官网上爬取paper列表为例。

获取

获取网页信息有很多种方法,在此我记录一下我遇到过的几种,并分析它们的用途特点。

方法一:selenium webdriver.get(url)

【推荐】

from selenium import webdriver
url="https://icml.cc/Conferences/2021/Schedule?type=Poster"
driver = webdriver.Firefox()
driver.get(url)
# 到这里已经算是获取到了网页信息,因为selenium库中的各种API支持对网页进行各种查询、操作

# 如果想进一步获得全部html代码
from selenium.webdriver.common.by import By
element = driver.find_element(By.XPATH,'/html')
html = element.get_attribute('outerHTML')

selenium的安装配置较为麻烦,它需要单独下载各个浏览器对应的driver(Chrome、Firefox、IE等很多浏览器都支持),还要配置系统环境变量。
但它的效果也是最好的。有些网页会动态生成内容,这时就只能伪装成用户,使用浏览器运行javascripts。当然js一般是自动运行的。不使用浏览器或js运行环境的方法,一般只能获得网页初始的静态内容。

方法二:requests

import requests        #导入requests包
url = "https://icml.cc/Conferences/2021/Schedule?type=Poster"
html = requests.get(url)        #Get方式获取网页数据
print(html.text)

以上是GET方法,POST方法我暂时还不懂。

方法三:urllib

from urllib import request
url = "https://icml.cc/Conferences/2021/Schedule?type=Poster"
html = request.urlopen(url).read()
html.decode()

方法四:手动

打开浏览器,输入目标url,Ctrl+S保存页面到.html,再在程序中读取文件。

with open('xxx.html','r') as f:
	html = f.read()
print(html)

解析

解析最重要的任务在于,找到想找的东西,不错找,不漏找。

什么是xpath

【推荐】
教程:w3school xpath
xpath大概长这样:/html/body/div[@class='container']
xpath甚至有内建函数,功能十分齐全。
注意,xpath中的索引从1开始。

什么是正则表达式

大概长这样:\d{1,3}(.\d{1,3}){3},这是表达IPv4地址的正则表达式(不过只限制了数字位数,没有限制三位数≤255)。

什么是selector

类似于xpath的一种html路径表达法,但感觉没有xpath支持性广。例如Firefox就复制不了selector。

方法一:selenium find_elements()

from selenium import webdriver
url="https://icml.cc/Conferences/2021/Schedule?type=Poster"
driver = webdriver.Firefox()
driver.get(url)
# 以上和html获取小节内容相同

from selenium.webdriver.common.by import By
xpath = "/html/body/div[@class='container pull-left']/div[@id='wholepage']/main[@id='main']/div[@class='container']/div[@class='row']/div[@class='col-xs-12 col-sm-9']/div[position()>3]/div"
driver.find_elements(By.XPATH,xpath)
# 这会返回一个列表,列表中的元素全部都是WebElement(selenium定义的变量类型)。

如使用find_elements则返回所有符合的元素,如使用find_element则返回第一个命中的元素。
但这种方法有一定问题。
1,浏览器放置时间久一点,selenium的API就会失去连接,而selenium的API只能对活动中的浏览器进行操控。
2,selenium没有提供针对html代码的解析程序。

方法二:lxml.etree

【推荐】

from lxml import etree
# html是html格式的字符串
Selector = etree.HTML(html)
xpath = "/html/body/div[@class='container pull-left']/div[@id='wholepage']/main[@id='main']/div[@class='container']/div[@class='row']/div[@class='col-xs-12 col-sm-9']/div[position()>3]/div"
Selector.xpath(xpath)

具体的用法我还没有仔细研究,但大体来说lxml是支持xpath的。
lxml本身也是BeautifulSoup的依赖库之一,其功能可以弥补selenium的缺点。

方法三:BeautifulSoup

from bs4 import BeautifulSoup
# 假设html为html格式的字符串
soup = BeautifulSoup(strhtml.text,'lxml') # 需要预先安装lxml
data = soup.select('#main>div>div.mtop.firstMod.clearfix>div.centerBox>ul.newsList>li>a') # 似乎不是xpath

BeautifulSoup和XPath的原理不一样,BeautifulSoup是基于DOM的,会载入整个文档,解析整个DOM树,因此时间和内存开销都会大很多。而lxml只会局部遍历。
就我个人的应用需求来说,BeautifulSoup不如selenium+lxml好用。

方法四:HTMLParser

from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):

    def handle_starttag(self, tag, attrs):
        """
        recognize start tag, like <div>
        :param tag:
        :param attrs:
        :return:
        """
        print("Encountered a start tag:", tag)

    def handle_endtag(self, tag):
        """
        recognize end tag, like </div>
        :param tag:
        :return:
        """
        print("Encountered an end tag :", tag)

    def handle_data(self, data):
        """
        recognize data, html content string
        :param data:
        :return:
        """
        print("Encountered some data  :", data)

    def handle_startendtag(self, tag, attrs):
        """
        recognize tag that without endtag, like <img />
        :param tag:
        :param attrs:
        :return:
        """
        print("Encountered startendtag :", tag)

    def handle_comment(self,data):
        """

        :param data:
        :return:
        """
        print("Encountered comment :", data)


parser = MyHTMLParser()
parser.feed('<html><head><title>Test</title></head>'
            '<body><h1>Parse me!</h1><img src = "" />'
            '<!-- comment --></body></html>')

(以上这段代码摘自Python爬虫常用之HtmlParser
HTMLParser是python自带的功能,但设计起来非常麻烦,需要继承HTMLParser类,进行新类的定义,并且五种方法重载也十分不人性化。
然而网上查到的方法很多都是这种,难道它们都是用爬虫在互相爬取然后复制么。

方法五:re(正则表达式)

import re
# html是html格式的字符串
pattern = r'<a href=[^>]*>(.*)</a>'
result = re.findall(pattern, html)

不多解释,把

方法N:手搓

之前走过一些弯路,当时只知道BeautifulSoup,不知道selenium和xpath,就照自己的想法创了一种语法,很像xpath。虽然现在看来没用了,但还是放出来供大家学习讨论。

from bs4 import BeautifulSoup
def ruleParse(rule):
    # 将字符串形式的rule解析成[{'name':'', 'attr': {} }, ... ]格式的内容
    # examples:
    # rule = "body.div(container pull-left).div(wholepage).main#main.div(container).div(row).div$2.div$3:-1"
    # rule = "body.div{class:[container, pull-left]}.div{class:wholepage}.main{id:main}.div(container).div(row).div{range:2}.div{range:(3,-1)}"
    # rule = "body.div$3.div$0.main.div$0.div$0.div$2.div$3:-1"
    rule_ = []
    i=0
    mode = 'word'
    string = ""
    splitters = "@#$(){}[]:."
    quoters = '\'"'
    quoter='"'
    spaces = " \t\n\r\b"
    while i<len(rule):
        char = rule[i]
        i+=1
        if mode=='word':
            if char in splitters:
                string = string.strip()
                if string!="":
                    rule_.append([string,'word'])
                string = ""
                rule_.append([char,'splitter'])
            elif char in quoters:
                string = string.strip()
                if string!="":
                    rule_.append([string,'word'])
                string = ""
                quoter = char
                mode='str'
            elif char in spaces:
                string = string.strip()
                if string!="":
                    rule_.append([string,'word'])
                string = ""
            elif char=="\\" and i!=len(rule):
                char = rule[i]
                i+=1
                string+=char
            else:
                string+=char
        else: # 'str' (quoted)
            if rule[i] == quoter:
                rule_.append([string,"str"])
                string=""
                mode='word'
            else:
                string+=char
    if mode!='word':
        raise "Syntax Error: unquoted string."
    else:
        string = string.strip()
        if string!="":
            rule_.append([string,'word'])
        string = ""
    rule__ = []
    stack=[]
    mode='name' # name class id range attr
    name=None
    class_=None
    id_=None
    range_=None
    attr={}
    def record():
        nonlocal name,class_,id_,range_,attr,stack,rule__
        if name==None:
            raise ValueError("Empty name")
        if class_!=None:
            attr['class']=class_
        if id_!=None:
            attr['id']=id_
        if range_!=None:
            if len(range_)==0:
                raise SyntaxError("Empty range")
            i=0
            while i<len(range_):
                item=range_[i]
                if item==':':
                    if i==0 or range_[i-1]==':':
                        range_.insert(i,None)
                        i+=1
                i+=1
            if range_[-1]==':':
                range_.append(None)
            range__=[]
            for item in range_:
                if item==':':
                    continue
                elif item==None:
                    range__.append(None)
                else:
                    range__.append(int(item))
            attr['$range']=range__
        rule__.append({
            'name':name,
            'attr':attr
        })
        name=None
        class_=None
        id_=None
        range_=None
        attr={}
    for content,typ in rule_:
        if mode=='name':
            if typ=='word':
                if name==None:
                    name=content
                else:
                    raise ValueError("Duplicated name")
            else: # typ == 'splitter'
                if content=='.':
                    record()
                elif content=='(':
                    mode='class'
                    stack=[]
                elif content=='#':
                    mode='id'
                elif content=='$':
                    mode='range'
                elif content=='{':
                    mode='attr'
                    stack=[]
                else:
                    raise ValueError(f'Invalid splitter "{content}"')
        elif mode=='class':
            if typ=='splitter':
                if content==')':
                    mode='name'
                    class_=stack.copy()
                    stack=[]
                else:
                    raise ValueError(f'Invalid splitter "{content}"')
            else: #typ=='word'
                stack.append(content)
        elif mode=='id':
            if typ=='splitter':
                raise SyntaxError(f'Unexpected splitter "{content}"')
            else: #typ=='word'
                if id_!=None:
                    raise SyntaxError('Duplicated ID')
                else:
                    id_=content
                    mode='name'
        elif mode=='range':
            if typ=='word':
                stack.append(content)
            else: # typ == 'splitter'
                if content=='.':
                    range_=stack.copy()
                    stack=[]
                    record()
                    mode='name'
                elif content in ',:':
                    stack.append(':')
        elif mode=='attr':
            if typ=='splitter':
                if content=="{":
                    pass
                elif content==':':
                    pass
                else:
                    raise ValueError(f'Invalid splitter "{content}"')
        else:
            raise ValueError(f'Invalid mode "{mode}"')
    if mode=='range':
        range_=stack.copy()
        stack=[]
    if mode in ['range','name']:
        record()
    else:
        raise SyntaxError("Unquoted content")
    return rule__

def getElements(source,rule: str):
    # 将受支持source转换成[BeutifulSouop]
    if type(source)==str:
        soups=[BeautifulSoup(source,features="lxml")]
    elif isinstance(source, Iterable):
        soups=list(source)
    elif type(source)==BeautifulSoup:
        soups=[source]
    else:
        raise TypeError("Invalid type for source.")
    parsedRule = ruleParse(rule)
    unalignedResultCnt=0
    for i in range(len(parsedRule)):
        item=parsedRule[i]
        name=item['name']
        attr=item['attr']
        def lambda_(tag):
            nonlocal name, attr
            if not tag.name==name:
                return False
            for key,value in attr.items():
                if key=='$range':
                    continue
                if not tag.has_attr(key):
                    return False
                if key=='class':
                    for class_ in value:
                        if not class_ in tag[key]:
                            return False
                else:
                    if not tag[key]==value:
                        return False
            return True
        results=[]
        resultCnt=0
        resultLen=None
        for soup in soups:
            result=soup.find_all(lambda_)
            resultCnt+=len(result)
            if resultLen==None:
                resultLen=len(result)
            else:
                if resultLen!=len(result):
                    unalignedResultCnt+=1
            results.append(result)
        if resultCnt==0:
            return []
        if '$range' in attr.keys():
            range_ = attr['$range']
            if len(range_)==1:
                index=range_[0]
                for i in range(len(results)):
                    results[i]=[results[i][index]]
            elif len(range_)==2:
                start, end=range_
                for i in range(len(results)):
                    results[i]=results[i][start:end]
            elif len(range_)==3:
                start,end,skip=range_
                for i in range(len(results)):
                    results[i]=results[i][start:end:skip]
            else:
                raise ValueError(f'Invalid range {range_}')
        else:
            pass
        soups=[]
        for result in results:
            soups+=result
    if unalignedResultCnt!=0:
        print(f"Warning: unaligned result number ({unalignedResultCnt})")
    return soups

# html是html格式的字符串
rule="body.div(container pull-left).div(wholepage).main#main.div(container).div(row).div$2.div$3:-1"
soups=getElements(html,rule)

效果还可以。但我刚写完,就在网上看到了xpath 😃 。


附:环境搭建

Windows: cmd/Powershell

推荐powershell。

selenium
pip install selenium
selenium官网
Firefox driver: geckodriver
访问geckodriver
Chrome driver
访问chromedriver镜像
IE driver: IEDriverServer
访问IEDriverServer
Edge driver: MicrosoftWebDriver
访问MicrosoftWebDriver
Opera driver
访问github: operachromiumdriver
PhantomJS driver: phantomjs
访问phantomjs

driver安装后需要把路径放入环境变量path中,或在使用时告知selenium具体的路径。

lxml
pip install lxml

BeautifulSoup
pip install beautifulsoup4

urllib
pip install urllib

requests
pip install requests

你可能需要用到conda来管理不同的python环境。

Linux/Mac: bash/zsh

没试过,应该跟windows上一样。严谨起见就分开写了。

posted @ 2021-11-27 14:33  小玄不要说话  阅读(264)  评论(0)    收藏  举报