Python高级应用程序设计任务

一、主题式网络爬虫设计方案(15分)

1.主题式网络爬虫名称

58同城房产抓取房产买卖信息
2.主题式网络爬虫爬取的内容与数据特征分析

本次爬虫主要爬取房产出售的位置、总价、面积、厅室等相关信息。
3.主题式网络爬虫设计方案概述(包括实现思路与技术难点)

思路:通过requests保持浏览器的登录状态,beautifulsoup对网页中的数据进行清洗,并且对采集到的相关信息进行解析,通过soup的select_one对网页源代码进行匹配从而找出其中想要的数据存到字段中,最后以csv格式文件保存。

难点:58同城具有反爬机制,提取58同城网页结构信息,和相关信息的提取,并且在爬取价格信息时,发现价格字段经过base64加密。

    二、主题页面的结构特征分析(15分)
1.主题页面的结构特征

https://np.58.com/ershoufang/?PGTID=0d200001-0283-332c-ca1b-1e5528072199&ClickID=1

包含了二手房的区域、总价、面积、厅室等信息。

2.Htmls页面解析

通过F12,查看网页源代码,查询相关信息。

3.节点(标签)查找方法与遍历方法
(必要时画出节点树结构)

  通过requests保持浏览器的登录状态,beautifulsoup对网页中的数据进行清洗,并且对采集到的相关信息进行解析,通过soup的select_one对网页源代码进行匹配从而找出其中想要的数据存到字段中。

三、网络爬虫程序设计(60分)
爬虫程序主体要包括以下各部分,要附源代码及较详细注释,并在每部分程序后面提供输出结果的截图。

  1 import requests
  2 import time
  3 import csv
  4 import re
  5 import bs4
  6 import os
  7 import logging
  8 import random
  9 import base64
 10 from urllib.parse import urljoin
 11 
 12 from fontTools.ttLib import ttFont, BytesIO
 13 
 14 # 用来维持cookie,保持登录状态
 15 session = requests.Session()
 16 # 设置用户代理和cookie,使58认为这是从浏览器去访问它的
 17 session.headers["User-Agent"] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Safari/605.1.15'
 18 session.headers["Cookie"] = 'commontopbar_ipcity=gllosangeles%7C%E6%B4%9B%E6%9D%89%E7%9F%B6%7C1; commontopbar_new_city_info=10291%7C%E5%8D%97%E5%B9%B3%7Cnp; f=n; als=0; wmda_session_id_11187958619315=1576494942212-5c6d2465-19f1-0b3e; wmda_visited_projects=%3B6333604277682%3B11187958619315; f=n; 58tj_uuid=fef049a4-34c6-42fb-9b5c-032c2de6a7a2; init_refer=; new_session=1; new_uv=1; spm=; utm_source=; wmda_new_uuid=1; wmda_session_id_6333604277682=1576494927127-6600b0cd-c4a6-6ce6; wmda_uuid=a862cf3bd206f96cab08a1c24772181b; JSESSIONID=CDCB6D81046D5476833EE19B97BEA52A; bj58_new_uv=1; bj58_id58s="OD1mblFRc2xpZkxJNTA0MA=="; id58=c5/nn12E1PkwRuPacj8eAg=='
 19 
 20 # 设置一个列表,存储地址、结果以及爬过的网址
 21 queue = [('https://np.58.com/ershoufang/pn2/', 'list', 0)]
 22 results = dict()
 23 footprint = dict()
 24 fields = set()
 25 
 26 # 反复爬取页面直到爬取完所有页面
 27 while len(queue) > 0:
 28     # 取出一个任务
 29     url, kind, retry = queue.pop(0)
 30     
 31     if url in footprint:
 32         continue
 33     # 爬取网页
 34     response = session.get(url)
 35     if response.status_code != 200:
 36         # 如果状态码不是200就是没有爬到,先跳过
 37         print("failed to crawl {}".format(url))
 38         # 重试5次
 39         if retry < 5:
 40             queue.append((url, kind, retry +1))
 41         continue
 42 
 43     if response.text.find("验证码") >= 0:
 44         # 遭遇验证码时休眠一会,避免因过于频繁操作而被认为是机器人
 45         print("need captcha, rest for a while")
 46         # 比如说,十分钟
 47         time.sleep(10*60)
 48         
 49         if retry < 5:
 50             # 如果重试次数小于5的话,输出访问的url,类型,并让重试次数+1,并随机的休眠5-7秒
 51             # 为了定位错误和输出错误信息
 52             queue.append((url, kind, retry + 1))
 53             time.sleep(2 + random.randint(3,5))
 54         continue
 55 
 56    
 57     print("crawled url {}".format(url))
 58 
 59     # 分析页面
 60     soup = bs4.BeautifulSoup(response.text, "html.parser")
 61     # 记录一下自己爬过了
 62     footprint[url] = True
 63 
 64     if kind == "list":
 65         # 如果是房屋列表页面
 66         # 把有关的链接放入队列中
 67         for link in soup.find_all("a"):
 68             new_url = urljoin(url, link.attrs.get("href", ""))
 69             new_url = new_url.split("?")[0]
 70    
 71             # pn 打头的都是目录,数字开头的都是房屋信息页
 72             if new_url.startswith("https://np.58.com/ershoufang/pn") and new_url not in footprint:
 73                 print("append list page into queue: {}".format(new_url))
 74                 queue.append((new_url, "list", 0))
 75             elif new_url.startswith("https://np.58.com/ershoufang/") and new_url not in footprint and re.match("https://np.58.com/ershoufang/[0-9]{3,}", new_url):
 76                 print("append detail page into queue: {}".format(new_url))
 77                 queue.append((new_url, "detail", 0))
 78     else:
 79         # 如果是房屋信息页面
 80         # 先提取字体
 81         fontBase64 = re.search("'data:application/font-ttf;charset=utf-8;base64,(?P<data>[^']*)'", response.text)
 82         # 如果房屋信息页面存在字体
 83         if fontBase64 is not None and 'data' in fontBase64.groupdict():
 84             # 从base64恢复字体文件
 85             fontRaw = base64.b64decode(fontBase64.groupdict()['data'])
 86             # 解析字体
 87             font = ttFont.TTFont(BytesIO(fontRaw))
 88 
 89             webdata = response.text
 90 
 91             # 找到字体对应数字的关系
 92             for code, name in font.getBestCmap().items():
 93                 # 用chr把对应的数字转换成html的unicode字符
 94                 ch = '&#x{:x};'.format(code)
 95                 # 58偷懒,glyph最后一位就是实际的数字
 96                 digit = int(name[-2:]) - 1
 97                 # 还原网页里面的所有unicode字符串
 98                 webdata = webdata.replace(ch, str(digit))
 99             
100             # 重新解析页面
101             soup = bs4.BeautifulSoup(webdata, "html.parser")
102         else:
103             print("shit, font is not avaible in ", url)
104 
105         results[url] = {
106                 'title': soup.select_one(".main-wrap .house-title h1").text.replace("\n", "").replace(" ", ""),
107                 'image': soup.select_one("#smainPic")["src"],
108                 'sumPrice': soup.select_one(".main-wrap .price").text.replace("\n", "").replace(" ", ""),
109                 'unitPrice': soup.select_one(".main-wrap .unit").text.replace("\n", "").replace(" ", ""),
110                 'houseRoom': soup.select_one(".room .main").text.replace("\n", "").replace(" ", ""),
111                 'houseFloor': soup.select_one(".room .sub").text.replace("\n", "").replace(" ", ""),
112                 'houseArea': soup.select_one(".area .main").text.replace("\n", "").replace(" ", ""),
113                 'houseFurnishing': soup.select_one(".area .sub").text.replace("\n", "").replace(" ", ""),
114                 'houseTorward': soup.select_one(".toward .main").text.replace("\n", "").replace(" ", ""),
115                 'houseBuildAt': soup.select_one(".toward .sub").text.replace("\n", "").replace(" ", ""),
116                 'houseAddress': soup.find(attrs={'class': 'c_999'}, text='位置:').find_next_sibling().text.replace("\n", '').replace(' ', ''),
117                 'housePrice': soup.find(attrs={'class': 'c_999'}, text='房屋总价').find_next_sibling().text.replace("\n", '').replace(' ', ''),
118         }
119         fields.update(results[url].keys())
120 
121     # 如果队列里面还有东西,停一下
122     if queue:
123         time.sleep(2 + random.randint(3, 5))
124 
125 # done
126 # 存储数据
127 
128 writer = csv.DictWriter(open("output.csv", "w+"), tuple(fields))
129 writer.writeheader()
130 writer.writerows(list(results.values()))

1.产生的文件

2.运行结果

(1)在终端上

(2)在csv中

1.数据爬取与采集

response = session.get(url)
    if response.status_code != 200:
        # 如果状态码不是200就是没有爬到,先跳过
        print("failed to crawl {}".format(url))
        # 重试5次
        if retry < 5:
            queue.append((url, kind, retry +1))
        continue

    if response.text.find("验证码") >= 0:
        # 遭遇验证码时休眠一会,避免因过于频繁操作而被认为是机器人
        print("need captcha, rest for a while")
        # 比如说,十分钟
        time.sleep(10*60)
        
        if retry < 5:
            # 如果重试次数小于5的话,输出访问的url,类型,并让重试次数+1,并随机的休眠5-7秒
            # 为了定位错误和输出错误信息
            queue.append((url, kind, retry + 1))
            time.sleep(2 + random.randint(3,5))
        continue

   
    print("crawled url {}".format(url))

2.数据分析

# 分析页面
    soup = bs4.BeautifulSoup(response.text, "html.parser")
    # 记录一下自己爬过了
    footprint[url] = True

    if kind == "list":
        # 如果是房屋列表页面
        # 把有关的链接放入队列中
        for link in soup.find_all("a"):
            new_url = urljoin(url, link.attrs.get("href", ""))
            new_url = new_url.split("?")[0]
   
            # pn 打头的都是目录,数字开头的都是房屋信息页
            if new_url.startswith("https://np.58.com/ershoufang/pn") and new_url not in footprint:
                print("append list page into queue: {}".format(new_url))
                queue.append((new_url, "list", 0))
            elif new_url.startswith("https://np.58.com/ershoufang/") and new_url not in footprint and re.match("https://np.58.com/ershoufang/[0-9]{3,}", new_url):
                print("append detail page into queue: {}".format(new_url))
                queue.append((new_url, "detail", 0))
    else:
        # 如果是房屋信息页面
        # 先提取字体
        fontBase64 = re.search("'data:application/font-ttf;charset=utf-8;base64,(?P<data>[^']*)'", response.text)
        # 如果房屋信息页面存在字体
        if fontBase64 is not None and 'data' in fontBase64.groupdict():
            # 从base64恢复字体文件
            fontRaw = base64.b64decode(fontBase64.groupdict()['data'])
            # 解析字体
            font = ttFont.TTFont(BytesIO(fontRaw))

            webdata = response.text

            # 找到字体对应数字的关系
            for code, name in font.getBestCmap().items():
                # 用chr把对应的数字转换成html的unicode字符
                ch = '&#x{:x};'.format(code)
                # 58偷懒,glyph最后一位就是实际的数字
                digit = int(name[-2:]) - 1
                # 还原网页里面的所有unicode字符串
                webdata = webdata.replace(ch, str(digit))
            
            # 重新解析页面
            soup = bs4.BeautifulSoup(webdata, "html.parser")
        else:
            print("shit, font is not avaible in ", url)

3.对数据进行清洗和处理

results[url] = {
                'title': soup.select_one(".main-wrap .house-title h1").text.replace("\n", "").replace(" ", ""),
                'image': soup.select_one("#smainPic")["src"],
                'sumPrice': soup.select_one(".main-wrap .price").text.replace("\n", "").replace(" ", ""),
                'unitPrice': soup.select_one(".main-wrap .unit").text.replace("\n", "").replace(" ", ""),
                'houseRoom': soup.select_one(".room .main").text.replace("\n", "").replace(" ", ""),
                'houseFloor': soup.select_one(".room .sub").text.replace("\n", "").replace(" ", ""),
                'houseArea': soup.select_one(".area .main").text.replace("\n", "").replace(" ", ""),
                'houseFurnishing': soup.select_one(".area .sub").text.replace("\n", "").replace(" ", ""),
                'houseTorward': soup.select_one(".toward .main").text.replace("\n", "").replace(" ", ""),
                'houseBuildAt': soup.select_one(".toward .sub").text.replace("\n", "").replace(" ", ""),
                'houseAddress': soup.find(attrs={'class': 'c_999'}, text='位置:').find_next_sibling().text.replace("\n", '').replace(' ', ''),
                'housePrice': soup.find(attrs={'class': 'c_999'}, text='房屋总价').find_next_sibling().text.replace("\n", '').replace(' ', ''),
        }
        fields.update(results[url].keys())

    # 如果队列里面还有东西,停一下
    if queue:
        time.sleep(2 + random.randint(3, 5))

 4.数据持久化

writer = csv.DictWriter(open("output.csv", "w+"), tuple(fields))
writer.writeheader()
writer.writerows(list(results.values()))

四、结论(10分)
1.经过对主题数据的分析与可视化,可以得到哪些结论?

  通过对58同城的页面爬取与分析,了解到南平房产出售的位置、总价、面积、厅室等等相关信息。

2.对本次程序设计任务完成的情况做一个简单的小结。

  通过本次爬虫的设计,对python的爬虫有了进一步的了解,也加深了requests、beautifulsoup的使用,并且采集到了58同城房产相关信息,学习到了如何爬取网页信息,及如何对加密信息进行解密,有了很大的收获。

posted @ 2019-12-18 15:22  李佳慧  阅读(365)  评论(0)    收藏  举报