网页爬虫开发实战——台部落

“台部落”对简书部分文章的未授权转载行为,想必大家已经有所耳闻了吧。

有简友查了一些关于爬虫的资料,里面提到”通过网络爬虫批量获取数据的成本较低“,这段话我部分认同。

既然”台部落“爬我们的文章,这次我们就以”台部落”为例,带大家了解一下网络爬虫的基本开发流程。

测试连通性

先给出台部落的网址:https://www.twblogs.net

如果大家直接访问的话应该会报错,因为这个网站使用了一个叫做 Cloudflare 的 CDN,通过设置禁止了所有大陆地区用户的访问。

CDN,全称 Content Delivery Networks,内容分发网络,是一种加速网络资源访问的技术。

假设简书的图片存放在上海的一个机房中,如果你在西藏,访问这张图片的速度就会比在上海慢一些,因为图片需要从更远的地方传输过来,这一方面是电信号的传输速度上限导致的,另一方面是因为数据在传输中会经过更多次处理,每次处理都要消耗一定时间。

所以,简书会将图片上传到 CDN 网络中,这样当西藏的用户访问时,就可以就近从西藏的服务器获取数据,加快了访问速度。当然,CDN 还有其它作用,比如降低数据传输成本、在一定程度上抵御网络攻击等。

直接访问网页都看不到数据,用爬虫当然也是采集不到的,所以我们需要通过一些特殊手段,将自己的 IP 更改成非大陆区域。出于监管原因,我不能描述具体方式,技术人自然会懂,普通用户不需要过多了解。

总之,经过一些操作后,我们可以正常访问台部落网站了。

爬虫程序一般使用 Python 开发。首先,我们需要确保程序可以正常访问到台部落网站。编写如下代码:

import requests

proxies = {
    "http": "socks5://127.0.0.1:10808",
    "https": "socks5://127.0.0.1:10808"
}

response = requests.get("https://www.twblogs.net", proxies=proxies)
print(response.text)

在这段代码中,我们导入了一个叫做 requests 的库,可以简单理解成别人写好的代码,然后设置本地代理,并通过代理访问台部落网站。

访问结果(称为“请求”)存储在 reponse 变量中,最后,获取请求到的文本内容(网页代码),通过 print 函数将其打印出来。

运行代码,输出如下:

现在我们已经成功通过程序访问了台部落。

分析网络请求

我们把目光放回到网页上:

这次我们要爬取的内容是每篇文章的标题、作者和发布时间。

对信息流内容的爬取,数据加载方式往往是突破口。

这个网站触发新内容加载的途径是下滑,也就是说,一定存在这样一个逻辑,在页面下拉到一定位置时触发新数据的加载。

而要加载内容,就不可避免地要发送网络请求。

我们按下 F12,打开浏览器的开发者工具,切换到网络选项卡,然后下滑页面触发刷新:

界面中显示出了非常多的网络请求,简单查看一下,大多数都是资源请求,比如网页中的图标和一些代码文件。

点击上图红圈中的“Fetch/XHR”按钮,筛选出所有异步网络请求,这是我们获取数据的关键:

注意这几条请求的网址,改变的只有最后的一部分,前面的“path=index&postoffset=”是没有变化的(这里的第一条请求来自一个浏览器扩展,可以忽略)。

点开第一条请求:

现在我们看到了数据加载背后请求的网址,我们一般将这种通过异步方式调用,返回数据的网址称为 API(应用程序编程接口,简称“接口”)。

接口地址下面是请求方式,这里是 GET,HTTP 请求中最常用的一种,我们平时访问某个网站,其实就是对网站发起 GET 请求。

接下来切换到“Payload”(负载)选项卡:

这些是请求所携带的参数,这个网站没有什么反爬措施,两个参数都不难理解。有些网站会在发送请求时携带一个验证用的参数(Token),而这个参数是会根据一定规则变化的,这时我们就需要对网页的源代码进行分析,找出 Token 的生成逻辑,这里不做展开。

“响应”选项卡,很明显是一段 HTML 代码。我们平时看到的网页,就是通过这种语言编写而成的。

另外,HTML 不是编程语言,而是一种“标记语言”,只靠它一个也不能做出美观的网页,还需要有 CSS 为网页“穿上衣服”,也就是调整格式,以及 JavaScript 实现网站的交互逻辑,比如点击标题时进入对应的文章页面。

使用程序发送网络请求

有了这些信息,我们就可以通过编写程序请求这个接口了。代码如下:

import requests

proxies = {
    "http": "socks5://127.0.0.1:10808",
    "https": "socks5://127.0.0.1:10808"
}

response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
                        proxies=proxies)
print(response.text)

程序输出如下:

很明显又是一段 HTML,我们略微修改代码,将数据保存到一个 HTML 文件中:

with open("twblogs_mainpage.html", "w") as f:
    f.write(response.text)

解析网页内容

用浏览器打开这个 HTML 文件:

这就是接口返回的结果,只这样看是看不出什么的,我们还是要回到源代码上,切换到“元素”选项卡:

开发者工具的左上角有一个方框和指针的按钮,点击一下,它会变成蓝色,然后点击网页上要采集的元素,开发者工具中的代码会自动展开,并显示出与这个元素对应的代码:

这里需要补充一点知识,HTML 是由一个个“标签”嵌套而成的,例如上图中的“html”、“body”、“div”、“section”。

每个标签都由尖括号包裹起来,不同种类的标签嵌套在一起,就构成了网页的骨架。

不难发现,我们的目标在body > section > h4 > a中,具体来说,是这个 a 标签的内容。

这里要引入一种叫做 xPath 的表达式,它可以帮助我们快速地在 HTML 中定位内容。

上面的嵌套关系,用 xPath 表示是这样的:

//section[@class="list-item"]/div/h4/a/text()

双斜杠的意思是查找网页中所有符合条件的标签,单斜杠则代表嵌套关系,中括号内可以指定这个标签的属性。

所以,这段 xPath 的含义是这样的:在整个 HTML 文件中查找 class 属性为 list-item 的 section 标签,定位到它下面的 div 标签,然后是 h4 标签,再里面是 a 标签,获取这个标签的内容。

我们可以通过一个叫做 xPath Helper 的浏览器插件来验证这段 xPath 的正确性:

result 中显示出了每篇文章的标题,接下来就是用程序实现解析流程了,我们需要用到一个叫做 lxml 的库,它可以帮助我们通过 xPath 表达式提取网页内容:

import requests
from lxml import etree

proxies = {
    "http": "socks5://127.0.0.1:10808",
    "https": "socks5://127.0.0.1:10808"
}

response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
                        proxies=proxies)
html_obj = etree.HTML(response.text)

print(html_obj.xpath("//section[@class='list-item']/div/h4/a/text()"))

程序以一个 Python 列表的方式返回了页面中所有的文章标题。

接下来我们再写一些 xPath 表达式:

# 文章链接
//section[@class="list-item"]/div/h4/a/@href

# 作者昵称
//section[@class="list-item"]/div/div/a/span/text()

# 作者链接
//section[@class="list-item"]/div/div/a/@href

# 发布时间
//section[@class="list-item"]/div/div/span/text()

然后修改我们的代码,使程序能获取到这些数据:

from datetime import datetime
from pprint import pprint

import requests
from lxml import etree

proxies = {
    "http": "socks5://127.0.0.1:10808",
    "https": "socks5://127.0.0.1:10808"
}

response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
                        proxies=proxies)
html_obj = etree.HTML(response.text)

titles = html_obj.xpath("//section[@class='list-item']/div/h4/a/text()")
article_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("//section[@class='list-item']/div/h4/a/@href"))
author_nicknames = html_obj.xpath("//section[@class='list-item']/div/div/a/span/text()")
author_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("//section[@class='list-item']/div/div/a/@href"))
publish_times = (datetime.fromisoformat(x) for x in html_obj.xpath("//section[@class='list-item']/div/div/span/text()"))

result = []
for title, article_link, author_nickname, author_link, publish_time in zip(
    titles, article_links, author_nicknames, author_links, publish_times):
    result.append({
        "title": title,
        "article_link": article_link,
        "author_nickname": author_nickname,
        "author_link": author_link,
        "publish_time": publish_time
    })

pprint(result)

在一番神奇的操作之后,程序以列表中嵌套字典的方式返回了我们要爬取的所有数据。

分页处理

但这只是一页,怎么获取其它页的内容呢?

还记得我们在前文看到的请求参数么?

offset,意思是“偏移”,看看前面爬取到的数据,这个字段像什么?

对,文章链接。每一个新请求所携带的 offset 参数值,正是上一个请求最后一组数据中,文章链接的最后一部分。沿用简书的叫法,我们将这一段称为 slug。

而 path 参数是不变的,这样问题就简单了,只需要存储每次请求获得的 slug,然后在下一次请求中作为参数传入即可。

简单修改一下代码,爬它十页:

from datetime import datetime
from pprint import pprint

import requests
from lxml import etree

proxies = {
    "http": "socks5://127.0.0.1:10808",
    "https": "socks5://127.0.0.1:10808"
}

slug = None
result = []
for i in range(10):
    params = {"path": "index", "postoffset": slug}
    response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
                            proxies=proxies)
    html_obj = etree.HTML(response.text)

    titles = html_obj.xpath("//section[@class='list-item']/div/h4/a/text()")
    article_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("//section[@class='list-item']/div/h4/a/@href"))
    author_nicknames = html_obj.xpath("//section[@class='list-item']/div/div/a/span/text()")
    author_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("//section[@class='list-item']/div/div/a/@href"))
    publish_times = (datetime.fromisoformat(x) for x in html_obj.xpath("//section[@class='list-item']/div/div/span/text()"))

    for title, article_link, author_nickname, author_link, publish_time in zip(
        titles, article_links, author_nicknames, author_links, publish_times):
        result.append({
            "title": title,
            "article_link": article_link,
            "author_nickname": author_nickname,
            "author_link": author_link,
            "publish_time": publish_time
        })

    slug = result[-1]["article_link"].split("/")[-1]
    print(f"爬取第 {i + 1} 页成功!")

print(f"数据条数:{len(result)}")

输出结果:

一百五十条数据,只用了十秒钟。

数据存储

result 列表在程序运行完毕之后就会消失,所以我们需要把数据进行存储。

一般情况下,我们使用数据库进行数据的存储,但考虑到代码复杂度和理解难度,我们这次使用 CSV 格式存储数据,可以理解为 Excel 表格。

只需要在最前面加上这行代码:

import pandas as pd

然后在最后加上这几行:

df = pd.DataFrame(result)
df.to_csv("data.csv", encoding="utf-8")
print("数据保存成功!")

程序运行完毕后,目录下会多出一个 data.csv 文件,我们打开它:

完美。

回头看看代码量,只有 44 行。

结语

十秒钟一百五十条数据,乍一看很快,但对于整个台部落的数据量来说,效率还是不尽如人意。

但我们编写的这种,是单线程同步爬虫。

在真正的爬虫程序中,可以同时发起数十个网络请求,可以让程序在发送一个请求后不在原地死等结果,而是去处理其它请求。

真实的数据库可以让多个程序同时写入数据,存储的数据量可达千万。

通过分布式技术,我们可以在多台服务器上部署爬虫程序,让结果统一存储到一个数据库内。

运用成熟的爬虫框架,我们可以更轻松地完成开发,并通过网页界面监控每个采集任务的状态。

生产数据的成本很高,但采集数据的成本很低。在技术防御之外,总需要有法律法规,对数据的使用方式进行限制。

利用无版权的商业数据获利,是不正当的商业竞争行为,做出这种行为的团体和公司,应当受到相应的处罚。

未经授权转载他人内容,属于侵犯著作权的行为。著作权的主体是公民,无论是否成年,你独立创作的内容,其著作权都属于你,你有权决定是否允许他人转载。

技术是否无罪,我不想做一个明确的判定,但我们共同期待着,期待着在创作领域,能有一束温暖的阳光穿过乌云,给予每个人应得的权利。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容