利用python爬虫学堂在线课程页面和链家二手房信息,分享一下经验

在python课上布置的作业,第一次进行爬虫,走了很多弯路,也学习到了很多知识,借此记录。

1. 获取学堂在线合作院校页面

要求

爬取学堂在线的计算机类课程页面内容。
要求将课程名称、老师、所属学校和选课人数信息,保存到一个csv文件中。
链接:https://www.xuetangx.com/search?query=&org=&classify=1&type=&status=&page=1

1.确定目标

打开页面,通过查看网页源代码并没有相关内容。可以猜测具体数据由前端通过ajax请求后端具体数据。在开发者工具中,捕获了如下的json数据:

Python爬虫项目报告 python爬虫课程报告_Python爬虫项目报告

可以看到这个就是我们要求的json数据。考虑如何获取json数据并取出来,分析一下浏览器的请求,将cURL命令转换成Python请求如下:

import requests

cookies = {
    'provider': 'xuetang',
    'django_language': 'zh',
}

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh',
    'Content-Type': 'application/json',
    'django-language': 'zh',
    'xtbz': 'xt',
    'x-client': 'web',
    'Origin': 'https://www.xuetangx.com',
    'Connection': 'keep-alive',
    'Referer': 'https://www.xuetangx.com/search?query=&org=&classify=1&type=&status=&page=1',
    'Pragma': 'no-cache',
    'Cache-Control': 'no-cache',
    'TE': 'Trailers',
}

params = (
    ('page', '1'),
)

data = '{query:,chief_org:[],classify:[1],selling_type:[],status:[],appid:10000}'

response = requests.post('https://www.xuetangx.com/api/v1/lms/get_product_list/', headers=headers, params=params, cookies=cookies, data=data)

#NB. Original query string below. It seems impossible to parse and
#reproduce query strings 100% accurately so the one below is given
#in case the reproduced version is not "correct".
# response = requests.post('https://www.xuetangx.com/api/v1/lms/get_product_list/?page=1', headers=headers, cookies=cookies, data=data)

分析请求的网页是https://curl.trillworks.com/,可以在浏览器的开发工具里,选择network选项卡(chrome)或者网络选项卡(Firefox),右键点击某个请求文件,在菜单中选择复制→复制为cURL命令,然后到这个网页中粘贴转换为python的request即可

2.设计爬虫

要选取的数据为课程名称、老师、所属学校和选课人数。设计的items.py如下:

# items.py
import scrapy


class XuetangItem(scrapy.Item):
    name = scrapy.Field()
    teachers = scrapy.Field()
    school = scrapy.Field()
    count = scrapy.Field()
    pass

接下来是重头戏设计spider.py文件。因为爬取的是json数据而不是html静态页面,需要设计start_requests函数来发送请求。结合之前分析的Python request,具体代码如下:

import scrapy
import json
from xuetang.items import XuetangItem


class mySpider(scrapy.spiders.Spider):
    name = "xuetang"
    allowed_domains = ["www.xuetangx.com/"]
    url = "url_pat = 'https://www.xuetangx.com/api/v1/lms/get_product_list/?page={}'"
    data = '{"query":"","chief_org":[],"classify":["1"],"selling_type":[],"status":[],"appid":10000}'
    # data由分析中得来
    headers = {
        'Host': 'www.xuetangx.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
        'authority': 'www.xuetangx.com',
        'Accept': 'application/json,text/plain,*/*',
        'Accept-Language': 'zh',
        'Accept-Encoding': 'gzip, deflate, br',
        'django-language': 'zh',
        'xtbz': 'xt',
        'content-type': 'application/json', # 如果不添加这一行可能导致爬虫失败
        'x-client': 'web',
        'Connection': 'keep-alive',
        'Referer': 'https://www.xuetangx.com/university/all',
        'Cookie': 'provider=xuetang; django_language=zh',
        'Pragma': 'no-cache',
        'Cache-Control': 'no-cache'
    }
    # 直接从浏览器抄过来,防止服务器辨析到不是浏览器而导致失败

    def start_requests(self):
        for page in range(1, 6):
            yield scrapy.FormRequest(
                url=self.url.format(page),
                headers=self.headers,
                method='POST',
                # 浏览器的请求是POST,而且响应头中写明只允许POST
                body=self.data,
                callback=self.parse
        )

     def parse(self, response):
        j = json.loads(response.body)
        for each in j['data']['org_list']:
            item = XuetangItem()
            item['name'] = each['name']
            item['school'] = each['org']['name']
            item['count'] = each['count']
            teacher_list = []
            for teacher in each['teacher']:
                teacher_list.append(teacher['name'])
            # 因为有些课程有多个老师,需要逐个保存,写入一条记录里
            item['teacher'] = ','.join(teacher_list)
            yield item

然后设计pipelines.py文件,将爬取到的数据保存为csv文件:

import csv


class XuetangPipeline(object):
    dict_data = {'data': []}

    def open_spider(self, spider):
        try:
            self.file = open('data.csv', "w", encoding="utf-8", newline='')
            self.csv = csv.writer(self.file)
        except Exception as err:
            print(err)

    def process_item(self, item, spider):
        self.csv.writerow(list(item.values()))
        return item

    def close_spider(self, spider):
        self.file.close()

这样就可以就行爬虫了,当然还要在setting.py中设置ITEM_PIPELINES。之后可以命令行启动爬虫,也可以运行执行cmd命令的python文件:

from scrapy import cmdline
cmdline.execute("scrapy crawl xuetang".split())

3.数据展示

保存的csv文件内容如下,正好内容为50条,这里仅展示开头一部分:

C++语言程序设计基础,清华大学,424718,"郑莉,李超,徐明星"
数据结构(上),清华大学,411298,邓俊辉
数据结构(下),清华大学,358804,邓俊辉
……

2. 获取链家二手房信息

要求:

爬取链家官网二手房的数据 https://bj.lianjia.com/ershoufang/ 要求爬取北京市东城、西城、海淀和朝阳四个城区的数据(每个区爬取5页),将楼盘名称、总价、平米数、单价保存到json文件中。

1.确定目标

打开网页,查看网页源代码,可以看到在源代码中间已经包含了二手房信息,说明页面由后端渲染完毕后返回到浏览器,这样可以通过Xpath来爬取相关内容。分析一下某个楼盘的信息结构:

<html>
 <head></head>
 <body>
  <a class="noresultRecommend img LOGCLICKDATA" href="https://bj.lianjia.com/ershoufang/101109392759.html" target="_blank" data-log_index="1" data-el="ershoufang" data-housecode="101109392759" data-is_focus="" data-sl="">
   <!-- 热推标签、埋点 -->
   <img src="https://s1.ljcdn.com/feroot/pc/asset/img/vr/vrgold.png?_v=202011171709034" class="vr_item" /><img class="lj-lazy" src="https://image1.ljcdn.com/110000-inspection/pc1_hAjksKeSW_1.jpg.296x216.jpg" data-original="https://image1.ljcdn.com/110000-inspection/pc1_hAjksKeSW_1.jpg.296x216.jpg" alt="北京西城长椿街" style="display: block;" /></a>
  <div class="info clear">
   <div class="title">
    <a class="" href="https://bj.lianjia.com/ershoufang/101109392759.html" target="_blank" data-log_index="1" data-el="ershoufang" data-housecode="101109392759" data-is_focus="" data-sl="">槐柏树街南里 南北通透两居室 精装修</a>
    <!-- 拆分标签 只留一个优先级最高的标签-->
    <span class="goodhouse_tag tagBlock">必看好房</span>
   </div>
   <div class="flood">
    <div class="positionInfo">
     <span class="positionIcon"></span>
     <a href="https://bj.lianjia.com/xiaoqu/1111027374889/" target="_blank" data-log_index="1" data-el="region">槐柏树街南里 </a> - 
     <a href="https://bj.lianjia.com/ershoufang/changchunjie/" target="_blank">长椿街</a> 
    </div>
   </div>
   <div class="address">
    <div class="houseInfo">
     <span class="houseIcon"></span>2室1厅 | 60.81平米 | 南 北 | 精装 | 中楼层(共6层) | 1991年建 | 板楼
    </div>
   </div>
   <div class="followInfo">
    <span class="starIcon"></span>226人关注 / 1个月以前发布
   </div>
   <div class="tag">
    <span class="subway">近地铁</span>
    <span class="isVrFutureHome">VR看装修</span>
    <span class="five">房本满两年</span>
    <span class="haskey">随时看房</span>
   </div>
   <div class="priceInfo">
    <div class="totalPrice">
     <span>600</span>万
    </div>
    <div class="unitPrice" data-hid="101109392759" data-rid="1111027374889" data-price="98668">
     <span>单价98668元/平米</span>
    </div>
   </div>
  </div>
  <div class="listButtonContainer">
   <div class="btn-follow followBtn" data-hid="101109392759">
    <span class="follow-text">关注</span>
   </div>
   <div class="compareBtn LOGCLICK" data-hid="101109392759" log-mod="101109392759" data-log_evtid="10230">
    加入对比
   </div>
  </div>
 </body>
</html>

可以看到房子的名称在class="title"的div下的a标签内,平米数保存在class="houseInfo"的div里,但需要截取一下字符串,单价和总价均保存在class="priceInfo"的div中,有趣的是有些信息没有单价显示,即span里的元素为空,但是观察到其父元素div内有一个属性data-price,其值正好等于单价,因此提取这个即可。

2.设计爬虫

需要保存的数据为楼盘名字、平米数、总价、单价。items.py如下:

import scrapy


class YijiaItem(scrapy.Item):
    # define the fields for your item here like:
    name = scrapy.Field()
    square = scrapy.Field()
    price = scrapy.Field()
    total = scrapy.Field()
    pass

分析要爬虫的页面,网页提供了选择区的筛选,点击“西城区”后网页地址变为了https://bj.lianjia.com/ershoufang/xicheng/,因此可以将网页地址的变动部分用format去填充。spider.py的内容如下:

from yijia.items import YijiaItem
import scrapy


class mySpider(scrapy.spiders.Spider):
    name = 'lianjia'
    allowed_domains = ["bj.lianjia.com/"]
    url = "https://bj.lianjia.com/ershoufang/{}/pg{}/"
    # 第一个地方为地区,第二个为页数
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
        'Connection': 'keep-alive',
        'Upgrade-Insecure-Requests': '1',
        'Pragma': 'no-cache',
        'Cache-Control': 'no-cache',
    }
    #抄来浏览器的header

    def start_requests(self):
        positions = ["dongceng", "xicheng", "chaoyang", "haidian"]
        for position in positions:
            for page in range(1, 6):
                yield scrapy.FormRequest(
                    url=self.url.format(position, page),
                    method="GET",
                    headers=self.headers,
                    callback=self.parse
                )

    def parse(self, response):
        for each in response.xpath("/html/body/div[4]/div[1]/ul/li"):
            item = YijiaItem()
            item['name'] = each.xpath("div[1]/div[1]/a/text()").extract()[0]
            house_info = each.xpath("div[1]/div[3]/div[1]/text()").extract()[0].split('|')
            item['square'] = house_info[1].strip()
            item['total'] = each.xpath("div[1]/div[6]/div[1]/span/text()").extract()[0] + "万元"
            item['price'] = each.xpath("div[1]/div[6]/div[2]/@data-price").extract()[0] + "元/平米"
            yield item

然后是设计管道文件,将内容保存为一个json文件:

import json


class YijiaPipeline(object):
    dict_data = {'data': []}

    def open_spider(self, spider):
        try:
            self.file = open('data.json', "w", encoding="utf-8")
        except Exception as err:
            print(err)

    def process_item(self, item, spider):
        dict_item = dict(item)
        self.dict_data['data'].append(dict_item)
        return item

    def close_spider(self, spider):
        self.file.write(json.dumps(self.dict_data, ensure_ascii=False, indent=4, separators=(',', ':')))
        self.file.close()

最后仿照前一个样例进行爬虫即可。

3.数据展示

保存的json文件内容如下所示,这里提供前两条供展示:

{
    "data":[
        {
            "name":"此房南北通透格局,采光视野无遮挡,交通便利",
            "square":"106.5平米",
            "total":"1136万元",
            "price":"106667元/平米"
        },
        {
            "name":"新安南里 南北通透 2层本房满五年唯一",
            "square":"55.08平米",
            "total":"565万元",
            "price":"102579元/平米"
        }
        /*省略之后的N条数据*/
    ]
}