C语言番外:如何用C语言做爬虫?
点击此处查看最新的网赚项目教程
内容提要:本文简单介绍爬虫的概念以及用 C 语言写一个爬虫示例程序。
什么是爬虫
百度百科:网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。
通俗一点讲,爬虫就是能从网络获取指定数据的程序。上面这段话中,“按一定的规则”,这个规则一方面是我们自己的代码定的规则,另一方面就是网络中的数据的格式;“自动地抓取”,既然是程序去抓取而不是我们手动打开浏览器看这些数据,那肯定就是“自动”了,当然如果是程序的话,我们也可以很方便的设定抓取时间。
举点例子吧,比如自己心仪某件数码产品很久了,奈何囊中羞涩,这时就可以写个程序自动抓取这个产品的价格,等他降价到自己可以接受的水平时程序给自己发一条短信告诉自己可以入手了;比如你每天午餐都是点外卖,但是你每天工作/学习又很忙,常常快到饭点了发现自己忘记点外卖了,那你也可以写一个程序每天 11:20 准时从你喜欢吃的几家外卖中随机点一个……
听起来是不是很酷?其实这样的程序远没有大家想的那么难,人类往往会高估陌生事物的难度。我希望我的读者们都可以变成无所不能的人!
今天我们一起用 C 语言写一个程序:每天早晨 7:00 获取当天的天气情况,如果可能会下雨,给自己的手机发一条推送,提醒自己出门时记得带伞。虽然可能很多同学还没有学习过任何关于网络、手机 App 的知识,但我还是希望你能坚持看完本文,完整个代码只有一百多行。
在开始写代码前,我们先理一下思路:
找一个可以获取天气信息的网站;
通过代码从该网站获取天气信息;
从该网站的天气信息响应内容中拿到我们想要的内容;
判断是否可能下雨;
给手机发送推送通知;
让程序定时执行。
一个可以获取天气信息的网站
我找到一个网站wttr.in。在浏览器中输入网址访问如下(默认显示的是北京的天气):
这个网站也提供了按城市查询天气的功能。比如本人在太原,只需要在网址后面跟上拼音Taiyuan,就会出现太原的天气信息了。
通过代码从该网站获取天气信息
再找一个网络库libcurl,这是一个免费且易用的客户端 URL 传输库,支持大多数常见的网络协议。有了这个库,我们就不需要直接在 C 语言中调用系统底层提供网络接口的 API 了,很多常用的协议这个库已经都封装好了。
这也是开源的一个意义,我们可以使用一些开源的代码来减少自己的工作量,而且避免了做一些重复性的工作。
在代码中使用libcurl提供的函数进行 HTTP 交互很简单,大概流程就是把一个网址传进去,网站的返回内容会存入我们程序中定义的缓冲区中。因为网站的返回内容其实是字符串,所以这里的缓冲区也就是char 数组。
/* 用来保存 HTTP 响应内容的数据结构 */
struct curl_fetch_st {
char *payload; // 用来保存响应内容的缓冲区
size_t size; // payload 这个字符串的长度
};
了解网络的同学可能会听过网络包这个概念,任何数据在网络中传输的时候都是被分成一个一个的网络包来传输的,这也是为什么下载一个文件的时候可以看到下载进度,也能暂停下载的原因。同样的,我们的 HTTP 响应内容可能也是被分成多分传回来的,每次回来一点响应数据,libcurl就会调用一下我们程序中的一个函数,直到所有响应数据接受完毕。这种函数被叫做回调函数,按照libcurl的要求写一个函数,把这个函数告诉libcurl。当libcurl收到 HTTP 响应内容时,就会调用我们指定的这个回调函数了。下面是本例中的回调函数:
size_t curl_callback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t realsize = size * nmemb;
struct curl_fetch_st *p = (struct curl_fetch_st *)userp;
char *temp = realloc(p->payload, p->size + realsize + 1);
if (temp == NULL) {
fprintf(stderr, "ERROR: Failed to expand buffer in curl_callback");
free(p->payload);
return 1;
}
p->payload = temp;
memcpy(&(p->payload[p->size]), contents, realsize);
p->size += realsize;
p->payload[p->size] = 0;
return realsize;
}
在这个回调函数中,先是用realloc增大了缓冲区的大小,然后用memcpy把新接收到的响应内容放到缓冲区中,并把缓冲区结构体的size属性设为新的大小。(如果实在理解不了这段代码也无所谓,因为这个回调函数的代码你可以复制到自己的项目中直接使用,即使你是从另外的网站请求数据)
然后封装了一个通用的发送 HTTP GET 请求的函数,本例中获取天气数据和给手机发送推送通知都需要用到 HTTP 请求,这两个地方可以都调用这个函数发送请求。
/*
* 向一个网站发送 HTTP GET 请求
* 参数 ch: libcurl 内置的代表一次请求的指针
* 参数 url: 请求的网址
* 参数 fetch: 自定义的接收响应的缓冲区
*/
CURLcode curl_get_request(CURL *ch, const char *url, struct curl_fetch_st *fetch) {
CURLcode rcode; // 返回值变量
// 申请缓冲区内存
fetch->payload = (char *)calloc(1, sizeof(fetch->payload));
if (fetch->payload == NULL) {
fprintf(stderr, "ERROR: Failed to allocate payload in curl_fetch_url");
return CURLE_FAILED_INIT;
}
fetch->size = 0;
// 下面都是调用 libcurl 提供的函数设置请求信息
// 设置网址
curl_easy_setopt(ch, CURLOPT_URL, url);
// 设置回调函数
curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, curl_callback);
// 设置回调函数的参数
curl_easy_setopt(ch, CURLOPT_WRITEDATA, (void *)fetch);
// 设置 UserAgent(客户端标识)
curl_easy_setopt(ch, CURLOPT_USERAGENT, "cs-tutor");
// 设置请求的超时时间
curl_easy_setopt(ch, CURLOPT_TIMEOUT, 15);
curl_easy_setopt(ch, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(ch, CURLOPT_MAXREDIRS, 1);
// 设置使用 HTTP 的 GET 方法
curl_easy_setopt(ch, CURLOPT_CUSTOMREQUEST, "GET");
struct curl_slist *headers = NULL;
// 设置 Accept 请求头
headers = curl_slist_append(headers, "Accept: application/json");
curl_easy_setopt(ch, CURLOPT_HTTPHEADER, headers);
// 发送请求
rcode = curl_easy_perform(ch);
// 发送完请求后,请求头占用的内存就可以释放了
curl_slist_free_all(headers);
return rcode;
}
这个函数的作用就是设置请求的网址,设置超时时间,然后把请求发送出去。对于还没有学过计算机网络相关课程的同学看不懂这段代码也没关系,这段代码也是通用的,可以复制到自己的项目中使用。
接下来就是发送第一个请求获取天气数据了,也是写到了一个函数里:
void request_weather_code(char *weatherCode) {
CURL *ch;
CURLcode rcode;
struct curl_fetch_st curl_fetch;
struct curl_fetch_st *cf = &curl_fetch;
// 定义请求的网址
char *url = "https://wttr.in/Taiyuan?format=j1";
// 初始化 CURL
if ((ch = curl_easy_init()) == NULL) {
fprintf(stderr, "ERROR: Failed to create curl handle in fetch_session");
return;
}
// 调用发送请求的函数
rcode = curl_get_request(ch, url, cf);
curl_easy_cleanup(ch);
// 根据 rcode 的值可以判断到在请求过程中有没有发生错误
if (rcode != CURLE_OK || cf->size < 1) {
fprintf(stderr, "ERROR: Failed to fetch url (%s) - curl said: %s", url, curl_easy_strerror(rcode));
return;
}
if (cf->payload != NULL) {
printf("CURL Returned: n%sn", cf->payload);
// 解析 JSON,在后面的小节中讲解
mjson_get_string(cf->payload, (int)cf->size - 1, "$.current_condition[0].weatherCode", weatherCode, 4);
free(cf->payload);
} else {
fprintf(stderr, "ERROR: Failed to populate payload");
free(cf->payload);
}
}
这个函数中先是定义了要请求的网址,然后调用前面写的curl_get_request()函数发送请求,请求发送后,缓冲区cf->payload中就是网页内容(HTTP 响应)了。
提取有用的数据
细心的同学可能会发现,我们的代码中的网址改成了,这是因为这个网站提供了多种响应内容格式。后面不加format=j1时,返回内容就像本文最头截图中的那样,那个网页中用字符串“画”出了天气情况和表格,这种格式非常适合我们人类阅读,但是对于程序来说就不太友好了。
在网络通信中,有两种便于机器识别的内容格式,分别是XML和JSON,没听过这两种格式的同学可以去搜索了解一下,然后思考一下为什么这两种格式的文本可以方便代码解析识别。
在wttr.in的网址中加上format=j1参数后,这个网站返回的响应内容就变成了JSON格式。如下:
{
"current_condition": [
{
"FeelsLikeC": "13",
"FeelsLikeF": "56",
"cloudcover": "0",
"humidity": "11",
"localObsDateTime": "2022-02-27 04:33 PM",
"observation_time": "08:33 AM",
"precipInches": "0.0",
"precipMM": "0.0",
"pressure": "1013",
"pressureInches": "30",
"temp_C": "13",
"temp_F": "55",
"uvIndex": "3",
"visibility": "10",
"visibilityMiles": "6",
"weatherCode": "113",
"weatherDesc": [
{
"value": "Sunny"
}
],
"weatherIconUrl": [
{
"value": ""
}
],
"winddir16Point": "SW",
"winddirDegree": "220",
"windspeedKmph": "7",
"windspeedMiles": "4"
}
],
在这段JSON中,可以找到一个weatherCode,是一个三位数的数字,不同的数字代表不同的天气。如果我们能拿到这个数字,就可以根据这个数字代表的天气判断出当天是否可能会下雨了。
我们当然不会自己写一个程序去获取weatherCode了,只需要像找libcurl那样,再找一个解析JSON的函数库即可。我找到一个叫mjson的函数库可以解析JSON,使用方法非常简单:调用mjson_get_string()函数即可,如下:
#include "mjson.h"
// 定义字符数组用于存储代表天气的数值
char weatherCode[4];
// 调用 mjson 提供的函数
mjson_get_string(cf->payload, (int)cf->size - 1, "$.current_condition[0].weatherCode", weatherCode, 4);
调用mjson_get_string()函数后,JSON字符串中的weatherCode的值就到了我们定义的字符数组weatherCode中了。
给自己的手机发一条推送
先看一下最终的效果图吧!
如果你用的也是iPhone,可以试一下我这个方案,先在 App Store 搜索安装一个 App: Bark。安装后,就可以通过这个 App 接收推送了,方法很简单,也是发送一个 HTTP 请求即可。
请求地址长这样:天气提醒/可能下雨记得带伞。其中,xxxxxxxx 部分每个手机不一样,安装好 Bark App 后,打开这个 App 就可以看到自己的手机对应的是什么;第二部分“天气提醒”是推送的标题,可以换成其他内容;第三部分“可能下雨记得带伞”为推送的内容,也可以换成其他内容。
至于发送推送的代码,和前面获取天气信息时是大同小异的:
void send_notification() {
CURL *ch;
CURLcode rcode;
struct curl_fetch_st curl_fetch;
struct curl_fetch_st *cf = &curl_fetch;
char *url = "https://api.day.app/tnwmuolgipqvhzll/天气提醒/可能下雨记得带伞";
if ((ch = curl_easy_init()) == NULL) {
fprintf(stderr, "ERROR: Failed to create curl handle in fetch_sessionn");
return;
}
rcode = curl_get_request(ch, url, cf);
curl_easy_cleanup(ch);
if (rcode != CURLE_OK || cf->size < 1) {
fprintf(stderr, "ERROR: Failed to fetch url (%s) - curl said: %s", url, curl_easy_strerror(rcode));
return;
}
if (cf->payload != NULL) {
printf("CURL Returned: n%sn", cf->payload);
// parse json
double code;
mjson_get_number(cf->payload, 999, "$.code", &code);
free(cf->payload);
if (fabs(code - 200) < 1E-6) {
printf("推送成功n");
} else {
fprintf(stderr, "推送失败n");
}
} else {
fprintf(stderr, "ERROR: Failed to populate payloadn");
free(cf->payload);
}
}
如果推送成功,这个请求会返回如下的响应内容:
{"code":200,"message":"success","timestamp":1646116130}
这也是JSON格式的,这次我们使用了mjson_get_number()函数从里面获取code的值,用这个值来判断推送是否成功。最后,看一下我们的main函数:
int main() {
// 定义字符数组用于存储代表天气的数值
char weatherCode[4];
// 请求天气信息
request_weather_code(weatherCode);
// 打印天气数值
printf("weatherCode is %sn", weatherCode);
// 判断是否可能下雨
if (is_rain_weather(weatherCode)) {
// 如果可能下雨,发送推送通知
send_notification();
} else {
printf("可以放心出门n");
}
return 0;
}
很好理解吧,其中is_rain_weather()函数就是根据代表天气的数字判断是否可能下雨:
/**
根据 weatherCode 判断是否代表可能会下雨
weatherCode 在 https://www.worldweatheronline.com/feed/wwoConditionCodes.txt 中定义
*/
int is_rain_weather(char *weatherCode) {
char *rainWathers[16] = {
"389", "386", "359", "356", "353",
"314", "311", "308", "305", "302",
"299", "296", "293", "266", "263",
"176"
};
for (int i = 0; i < 16; i++) {
if (strcmp(weatherCode, rainWathers[i]) == 0) {
return 1;
}
}
return 0;
}
代码部分到这里就讲完了,可能很多初学 C 语言的同学还是有一些不懂的地方,这都没关系,只要你理解这个程序的大致思路就可以了。希望目前还在校学习的读者们可以着重培养一下自己的动手能力。当我们遇到一个问题,在 C 语言中如何发送 HTTP 请求?或者如何解析 JSON?我们在网上找资料,于是就找到了libcurl、mjson这两个库,接下来就是如何使用这两个库的问题?现在互联网资源这么丰富,网上总是能找到示例的。我在写这篇公众号时也是第一次使用libcurl和mjson。
libcurl是 C 语言里面一个非常标准的网络库,很多开发工具安装时就会把这个库安装到系统里,在这种情况下,使用时需要先引入头文件#include (如果有警告说找不到这个头文件,那你就需要自行把这个库安装到系统上了),编译时,需要添加编译参数-lcurl,这个参数是告诉gcc要连接curl库。
mjson是我在GitHub上找的一个可以解析JSON的库,我这里没有把它安装到系统上,所以只是把mjson.h和mjson.c两个文件复制到自己的项目里使用的,这时候在main.c文件中是通过#include "mjson.h"引入相关的功能的。
让程序定时执行(Linux版)
Linux 系统提供了一个叫 crond 的服务用来管理定时任务,在介绍crond前,先介绍一下cron表达式,cron表达式是业界通用的一种可以表示定时时机的表达式。比如代表每天早上 7 点的cron表达式为:0 7 * * *,这个表达式可以用空格分成 5 部分,第一部分代表分钟,这里用 0 就代表 7 点整,如果是 7 点半就写 30;第二部分代表小时,几点执行就写几;第三部分代表一个月中的日,这里如果写 1 就代表每个月 1 日执行;第四部分代表月,几月执行就写几;第五部分代表星期,星期几执行就写几。所以对于宅男来说,这个表达式写成0 7 * * 1,2,3,4,5,代表周一到周五的早上 7 点(周末宅家)。
有些地方cron表达式前面还会有一部分代表秒的,后面有一部分代表年的。
crond也很简单,输入命令crontab -e后,系统会让你输入一个cron表达式外加一个命令。这里的命令就是到达指定执行时间后系统要执行的操作。比如我们将示例程序编译、连接生成的可执行文件命名为wttr,那我们的输入就是:
0 7 * * 1,2,3,4,5 wttr
当然,最好是像我们之前在中讲的那样,把我们代码中输出到stdout和stderr日志保存下来:
0 7 * * 1,2,3,4,5 wttr 1>/var/log/wttr.log 2>/var/log/wttr.error.log
写到这里,我突然想到了一个我小时候非常喜欢做的一个白日梦:如果我有一个双胞胎兄弟该多好,让他替我上学,我就可以在外面玩。其实现在想想,如果自己不愿意去做的事,写一个程序替自己去做也不是不可以,也算是圆了儿时的白日梦了。试想一下,除了程序员,还有谁能做到这一点呢?我在这里也恭喜各位选择了计算机软件这么炫酷的专业!
未来,无限可能!
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: qs62318888
主题授权提示:请在后台主题设置-主题授权-激活主题的正版授权,授权购买:RiTheme官网