标准化爬虫程序分享

前言

笔者趁最近闲暇之余,写了一个 400 行的标准化爬虫程序,可以说是大大加快了爬虫的速度,只需要 3-30 秒就可以获取网页页面显示文字的标准路径,并完成路径下完整信息的爬取,将针对某一类网页构建标准化爬虫流程的速度提高 100 倍以上。作为 Python 入门者也可以很快上手这一套程序。

例如:设定网页爬取目标为 https://sz.lianjia.com/ershoufang/rs红树湾/,设定追踪文字 [‘200平大四房’,‘中信红树湾北区204平’],0.68 秒后程序返回结果:

Crawed contents:
[' 200平大四房  端头三面采光,看高尔夫和世界之窗内湖', ' 世纪村二期 4室2厅 1300万', ' 中信红树湾北区204平大四房 安静南北通装修保养好', ' 世纪村二期153平3+1房,25平入户花园', ' 红树西岸正南正北大三房  看海景', ' 高楼层,新装修,看海景园林景观,满五唯一,钥匙看房', ' 美庐锦园 3室2厅 930万', ' 红树西岸大三房  呓高尔夫看海景', ' 东南向无西晒 厨房带采光阳台 位置安静离滨海大道远', ' 西岸4房改精装大两房 看全景高尔夫 满五唯一 随时看房', ' 南北通 四房 客厅带阳台东南  看海景 厨房全明 少放盘', ' 百仕达红树西岸 2室1厅 1690万', ' 中信红树湾南区 4室2厅 1980万', ' 世纪村四期,日月府,大4房格局,183.02平米,精装修', ' 御景东方1栋三房 三个房间都朝南 客厅出阳台', ' 中信红树湾南区  4房加入户和中庭花园,可以做6房', ' 毛坯4房,看海+高尔夫,看房我有钥匙', ' 中信北区 高层 复试 看房方便', ' 地铁口物业,端头位三面采光,买两房', ' 百仕达红树西岸 3室2厅 2450万', ' 红树西岸南向202平大三房,看一线海景', ' 世纪村4+1房 客厅和主卧出阳台 南北通 诚心出售', ' 京基御景东方高层带连廊精装修推荐!', ' 带20平左右中庭花园 可做5房三套间 大户型物业', ' 中信红树湾 大平面 装修保养好 看海景 高尔夫景', ' 高楼层厅出阳台 东南向 南北通透四房 装修保养好', ' 世纪村二期精装三房 小区中间位置 安静舒适', ' 可观全景高尔夫的三居室,有钥匙,看房方便', ' 红树湾南区精装3房,安静开阔视野好。', ' 红树湾宜居三房 刚需入门户型 精装修端头位置']

尽管现在市面上已有非常多用于爬虫的软件,同样可实现以上一键操作。但作为使用者,我们无法了解和修改黑箱里的具体操作和规则。这也是笔者能带给您方便的地方。

介绍

完成一套程序基本可走完以下流程:

  • Step 1: 爬取网页,以树的形式解析 HTML 结构
  • Step 2: 根据指定文本,获取多条文本路径
  • Step 3: 从多条文本路径中,通过算法求得最优标准路径
  • Step 4: 遵循标准路径,爬取所有可获取信息

其中 Step 2 和 Step 4 应用了深度优先搜索,Step 3 应用了动态规划求解多维最长公共子序列的算法以及正则表达式。如果您需要跳过详细介绍直接获取完整代码,请直接跳到本文最后一节。

一、解析网页

第一步是定义节点类。HTML 本身以树的形式来承载数据,因此使用树的数据结构最为合适。

class Node(dict):
    '''Node nodeect recording node information.'''
    
    def __init__(self):
        self.dict = dict()
        
    def __getitem__(self,key):
        '''Create a new child node as default.'''
        if key not in self.dict.keys():
            self.dict[key] = Node()
        return self.dict[key]
    
    def __str__(self):
        '''Print child nodes.'''
        return "Node with child nodes: %s \n(try to get the whole map by removing print function)"%', '.join(list(self.dict.keys()))
    
    def keys(self):
        '''Return the keys.'''
        return self.dict.keys()
    
    def add(self,key,value):
        '''Add key and value within a node.'''
        if key not in self.keys():
            self.dict[key] = value
        else:
            self.dict[key] += value

每一个节点对象承载两类信息:子节点和文字。随后,定义爬虫框架类 MyCrawler:

HEADERS = {
   "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36"}
ATTEMPT_ENCODING = ('utf-8', 'gbk')
PRINT_INFO = True
PRINT_WARNING = True

class MyCrawler:
    '''Class recording crawling details.'''
    
    def __init__(self,headers=HEADERS,cookies=None,timeout=5,print_info=PRINT_INFO,print_warning=PRINT_WARNING):
        self.url = None
        self.html = None
        self.headers = headers
        self.cookies = cookies
        self.timeout = timeout
        self.print_info = print_info
        self.print_warning = print_warning

接下来是解析网页的相关代码,直接补充在 Crawler 类下。缺了什么模块,读者直接在代码文件最上方 import 即可。

    def parse(self,url):
        '''Main process for crawling and parsing the whole html file.'''
        self.url = url
        response = requests.get(self.url, headers=self.headers, cookies=self.cookies, timeout=self.timeout) 
        for encoding in ATTEMPT_ENCODING:         # attempt on different encoding type
            try:
                self.html = ' '*10 + response.content.decode(encoding) + ' '         # blank: prevent from index out of range
                break
            except:
                continue
        self.html = self.html.replace('\n',' ').replace('\r',' ')
        self.top_node = Node()
        special_tags = set(re.findall(r'<([^ ^>^!^/]+?)[ >]',self.html))
        for tail in set(re.findall(r'</[^ ^<^>^!]+>',self.html)):
            special_tags -= {
   tail[2:-1]}         # filter closed opening tags without corresponding ending tags
        i,j,path = 0,0,[]
        on_comments = on_script = on_single_quote = on_double_quote = in_tag = False
        while i < len(self.html) - 1:
            if on_comments:
                # skip when scanning on comments
                if (self.html[i-2:i+1] == '-->'):
                    on_comments = False
                    j = i
            elif on_script:
                # skip when scanning on javascript scripts
                if (self.html[i-8:i+1] == '</script>'):
                    on_script = False
                    node = self.follow(path)
                    node.add('text',' '+self.html[j+1:i-8])
                    path = path[:-1]
                    j = i
            elif (self.html[i] == "'") & (not on_double_quote) & in_tag:
                on_single_quote = not on_single_quote
            elif (self.html[i] == '"') & (not on_single_quote) & in_tag:
                on_double_quote = not on_double_quote
            elif (not on_single_quote) & (not on_double_quote):
                if (self.html[i] == '<'):
                    if (len(path) > 0):
                        # record any content
                        node = self.follow(path)
                        node.add('text',' '+self.html[j+1:i])
                    if (self.html[i+1] != '/'):
                        if (self.html[i+1] != '!'):
                            # start recording opening tag: --> <div> 
                            in_tag = True
                            j = i
                        else:
                            # skipping from comments: <!--... -->
                            if (self.html[i+1:i+4] == '!--'):
                                on_comments = True
                    else:
                        # start recording ending tag: --> </div>
                        in_tag = True
                        j = i
                elif (self.html[i] == '>') & (self.html[j] == '<'):
                    in_tag = False
                    tag_name = re.findall(r'<([^ ^>]+?)[> ]',self.html[j:i+1])[0]
                    if (len(re.findall(r' content="',self.html[j:i+1])) > 0) or (tag_name in special_tags) or ((tag_name != 'script') & (self.html[i-1] == '/')):
                        # stop recording special node and start recording contents: ![在这里插入图片描述]() -->
                        node = self.follow(path)
                        node.add(self.html[j:i+1],'')
                        j = i                         
                    elif (self.html[j+1] != '/'):
                        # stop recording the opening tag and start recording contents: <div> -->
                        path.append(self.html[j:i+1])
                        if (tag_name == 'script'):
                            on_script = True
                        j = i
                    elif (self.html[j+1] == '/'):
                        # stop recording the ending tag: </div> -->
                        if re.findall(r'<([^ ]+?)[ >]',path[-1])[0] == tag_name[1:]:
                            path = path[:-1]
                            j = i
                        else:
                            try:
                                s = -1
                                while True:
                                    if (re.findall(r'<([^ ]+?)[ >]',path[s])[0] == tag_name[1:]):
                                        if self.print_warning: print("Warning: skipping excessive opening tags %s in order to match %s."%(', '.join(path[s+1:]),self.html[j:i+1]))
                                        path = path[:s]
                                        break
                                    else:
                                        s -= 1
                            except:
                                if self.print_warning: print("Warning: ignoring unmatched ending tag %s."%self.html[j:i+1])
                            j = i
            i += 1
        if len(path) > 0: 
            if self.print_warning: print("Warning: unexpected structure of HTML. Please carefully check the completeness of HTML file. If it doesn't work, please ask help from developer. \n(unclosed opening tags: %s)"%', '.join(path)) 

这样一来,网页解析便完成了,我们可以试试效果:

if __name__ == '__main__':

    # basic setting
    url = 'https://sz.lianjia.com/ershoufang/rs%E7%BA%A2%E6%A0%91%E6%B9%BE/'
    
    # basic crawling for parsing the HTML file
    crawler = MyCrawler()
    crawler.parse(url)
    print(crawler.html)
  	print(crawler.top_node.keys())

不出意外的话,您能看到原 HTML 文件,以及解析后的树第一个节点的子节点标签。如果出现了 Warning 开头的字段,说明在解析 HTML 的过程中出现问题,这些问题通常不是由笔者的程序本身造成的,而是网页设计者本身的缺陷,比如不合时宜地多出一个引号,或者多出一个毫无意义的标签。这些问题通常不会对程序造成影响(否则你也无法通过浏览器看到网页内容),但如果数量过大,例如大于 10 条,那么就要小心了(这样的情况笔者在测试过程中还未出现过)。针对以上问题,笔者在 MyCrawler 类下新添加了一个用于 debug 的类:

    
    def debug_count(self):
        '''To: Check the number of opening tag and ending tags using a same name.
        ----------------------
        e.g. >>> crawler.debug_count('')
             <div>: opening 103, ending 103.
             ...
        Normally, each pair of two numbers should be equal, or there's somewhere wrong
        in the HTML file.'''
        opening_tags = set(re.findall(r'<([^ ^>^!^/]+?)[ >]',self.html))
        ending_tags = set(re.findall(r'</([^ ^<^>^!]+?)>',self.html))
        for tag in (opening_tags & ending_tags):
            opening = len(re.findall(r'<%s[ >]'%tag,self.html))
            ending = len(re.findall(r'</%s>'%tag,self.html))
            print('<%s>: opening %d, ending %d.'%(tag,opening,ending))

使用规则很简单:

	crawler.debug_count()

运行以上代码,程序会统计原 HTML 中节点出现的次数。具体的使用规则这里很难解释清楚,读者可使用后自行体会。

二、获取路径

接下来的代码同样用于 MyCrawler 类,实现给定文字,获取节点路径的功能。

    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值