Category Archives: 编程

写一个 Python 爬虫

写一个 Python 爬虫其实挺容易的,也许过程中需要不停的调试,但爬虫的代码绝对短小精悍!

肇事者爬虫

这就是我昨天把郑子涵五子棋网站搞挂的 Python 爬虫,文件名为 spider004.py。没错是我写的第四个爬虫…今天为了防止我继续恶意开房,他给网站加了验证码。目前我还无能为力= =

HTTP 协议

要写一个爬虫,首先你得懂 HTTP 协议的一些东西。我当时是看了这篇文章《HTTP协议详解》。我简单说下自己的理解:

当你用浏览器访问网页的时候,其实是经历了这么一系列的过程:你通过浏览器发出 HTTP 请求 (HTTP Request),发送请求有多种方法,常见的两种是 GET 和 POST。网页的服务器端收到你的请求进行处理后,就会给浏览器响应 (HTTP Response)。而 HTTP Request 和 HTTP Response 都是有参数哒,你可以通过 F12 来看。

这很好理解,因为交互各种各样,访问不同的网址,是否登录账号,用什么浏览器,甚至在不同的时间发起请求,都会导致 HTTP Request 的不同。而服务器端挂了,状态码(status_code) 可能是 502 Bad Gateway,又或者 404 Not Found,这都是平时常见的错误,就是 HTTP Response 的一部分。(你可能会把它统称为:“ 啊,网页怎么打不开了!”,其实打不开也是有不同的原因的)。

差不多先了解这些,之后写爬虫的过程中,我们还会常用 F12,利用 HTTP Request/Response 的信息来调试。

Python Requests Module

所谓的网页爬虫,就是模仿浏览器向服务器发送特定的请求,并且返回获取的内容。而 Python 为此提供了一个很好用的模块,Requests 模块。

我最早是看了这个教程《HTML Scraping》,并且按照上面操作了一遍。这个教程提到抓取网页内容可以用的两种定位方法 XPath 和 CSSSelect,虽然我都没学会..

然后还看了这两个教程,一个是《Requests: HTTP for Humans》官方文档,另一个是《Using Requests in Python》简单教程。

我们可以简单的演示一下(终端中打开python3):

>>> import requests
>>> page = requests.get("http://www.baidu.com")
>>> page.url
'http://www.baidu.com/'
>>> page.status_code
200
>>> page.headers
(省略)
>>> page.text
(省略)

如果你需要的是抓取网站获取的信息(显示的HTML文件),可以把 page.text 部分输出到文件中。

把网站搞垮的具体细节

我观察到五子棋网站下棋开的房间是随机6位数,而且保存所有的棋盘信息,也就是你下完棋哪怕两个人都退出了以后,房间还是被占用的,你们还可以之后再进去继续玩。也就是,“如果我把所有的房间都开满了,那其他人没法开新的房间,也没法再已经被我开的房间里下棋了”←我对网站的攻击就是基于这个思路。

首先,自己打开浏览器里的匿名窗口操作一遍(匿名窗口是防止 cookie 混乱),并且按F12打开控制台, 观察到我新建房间时发出的请求获取的文件,如

戳进去,拿到 cookie 和 url,然后作为代码的参数

我把它写成了死循环…然后,运行这个代码。每次访问http://zzh.freeshell.ustc.edu.cn/game/newroom.php这个url,它就会返回一个新房间的地址,相当于完成了开房的动作。

以上这小段代码也是我经过一会时间修改才搞定的…

确认可行之后,我开了10个终端…一起刷,结果一个多小时后,网站就挂了。。。整个 freeshell 登不上去了。经神秘管理员重启之后,网页上已经无法新建房间,随机输入一个房间号都有一个叫做 root 的玩家在里面。

-78cbf7ee7ebe5a37

后续情节…

肇事者表示刚刚学会写爬虫所以拿了个好朋友的网站来玩…神秘的 freeshell 管理员其实是帮凶,他不仅指导我写脚本,而且在我开始攻击之后,突发奇想想要测试该 freeshell 的抗压能力…搞得该站长一直以为是有人通过 2081号 freeshell 攻击。我做了坏事,看对方没有反应,先是主动上去报了 bug(“哎呀,你的五子棋没法玩了耶”→好贱),最后被查出来以后主动承认并道歉…我表示一开始还挺兴奋,后面觉得这么做还是挺不好的…要是对方生气了呢,是吧?

【C语言笔试复习】指针数组和数组指针

这周开始做C语言笔试卷,什么嘛!考的都是概念,各种细节,虽然貌似书上都能找到,但我还真不清楚…顿时又有一种我C语言学的好渣的感觉 T_T

好了,说下在【上机实验蓝皮书背后,综合测试题二】中难到的2道题,这两道题使得你对【指针数组】和【数组指针】的区别更加清晰。

【例题1】

有以下程序:

#include 
 
int main()
{
    char *s[] = {"one", "two", "three"}, *p;
    p = s[1];
    printf("%c, %s\n",*(p+1), s[0]);
    return 0;
}

执行后的结果是_______。

A. n, two B. w, one C. t, one  D. o, two

第一次我选了 C,因为我以为 (p+1) 是指向 “three“ 的。这是对指针 p 类型的理解错误。

正确答案是 B,因为:

 #include 
 
 int main()
 {
     char *s[] = {"one", "two", "three"}; // s是一个指针数组,元素是3
                                          // 个指向字符串常量的指针
     char *p = s[1];  // p是一个指向字符串的指针变量
     printf("%c, %s\n",*(p+1), s[0]);
     // (p+1)是p的地址加一个字符内存的大小,从指向t变成指向w
     return 0;
}

所以,平时我们说的指向字符串的指针,其实都是指向一个字符,所以对它进行位移运算时,加减都是1。
另外,如果你把第8行

   printf("%c, %s\n",*(p+1), s[0]);

改成

   printf("%c, %s\n",*p, s[0]);

输出就会是:wo,one

因为平时我们输出字符串的时候,实际上都是把字符串的首地址传给 printf( ) 函数,它通过末尾的 ‘\n’,来判断是否结束。

【例题2】

#include 

int main()
{
    int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
    int (*p)[4] = a;
    printf("%d\n", *(*(p+1)+3));
    return 0;
}

以上代码执行的结果是_______。

答案是 8

这道题初看我完全不理解…怎么*了还能*,(*p)[4]又是什么鬼…我做这份卷子前把【指针数组】和【数组指针】搞乱了=

是这样的:

1.(*p)[4]:声明p是一个指向(4个int元素的数组)的指针,因此 p+1 从指向 a[0],变成指向 a[1]

2.*(*(p+1)+3):为什么会有两个星?

*(p+1) => a[1][0](也就是5),第一星从a[1]变成a[1][0],这个过程虽然地址没有变,但指针的类型变了!原来指针+1是加4个 int,现在指针+1只加一个 int 了!!

*(p+1)+3 => a[1][3](也就是8),再取一个*就是从地址里读出8

还可以修改一下原来的代码弄清楚:

 #include 
 
 int main()
 {
     int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
     int (*p)[4] = a;   
     printf("p: %p\n",p);
     printf("p+1: %p\n",p+1);
     printf("*(p+1): %p\n",*(p+1));
     printf("*(p+1)+3: %p\n",*(p+1)+3);
     printf("%d\n",*(*(p+1)+3));
     return 0;
}

输出的结果是:

p: 0x7fff9f4ba180
p+1: 0x7fff9f4ba190
*(p+1): 0x7fff9f4ba190
*(p+1)+3: 0x7fff9f4ba19c
8

好啦~

这篇笔记最早发在博客园里,直接粘贴过来再排版的,所以格式不太美观,可以看→原文

C指针学习吐槽

上周我一直在通过看书、实践来试图弄懂指针。

一直到学习指针之前,我几乎都是不看书的。也没有看老师上课的PPT,因为感觉老师的PPT经常有错误,上课用还凑合,但自己课下看实在是很不舒服。我手头上有两本教材,一本是科大自己的《计算机程序设计(C语言版)》(机械工业出版社的),另一本是C语言经典教材《C程序设计语言(第二版)》是 The C Programming Language 翻译过来的。

按说第二本显然更好。因为LUG里面众人的吐槽和推荐,我不是很相信学校的教材,你看全书代码风格都不统一。而第二本虽说好,之前在阮一峰的博文 C语言学习教材 上面看到,并不适合新手。= =

上周C指针的学习,一开始是看经典教材的,结果看到字符指针就看不懂了,主要是作者为了引入字符指针举了个非常麻烦的例子。几乎看不懂!!

我发现C语言教材有这样的问题,它假设你之前的部分都看过了,所以不时的引入之前的内容而且不附加说明。造成的结果,你即使按照教材上面的代码输了一遍也往往跑不起来。

反正,最后放弃 The C Programming Language 我也认为它太难,太生涩,对新手不友好!相反,上周最后发现,学校的这本教材对指针的解释还可以接受=。就决定看这个了,感觉C指针还是需要资料的指导的。

所以,不仅人和人之间的信任是很重要的,对书本的信任也是很重要的…

看地址学指针2:字符指针

字符指针,也就是指向字符数组(字符串常量)的指针,比起指向数组的指针有略微不同。似乎是因为字符串常量特有的性质。还是通过代码来说明好了。

字符数组

11261

11262

以上操作,并没有体现出字符数组和之前我们学过的数组有什么不同。我只是想看看字符数组的地址啦~(看地址强迫症)

指向字符数组的指针

先回顾一下指向数组的指针

112611

pa 是指向数组 a 的指针变量

112612

pgreeting 是指向字符数组 greeting 的指针变量

112613

所以,从输出来看,指向数组的指针变量和指向字符数组的指针变量没什么区别嘛!输出的地址都差不多长= = 其他的一些操作也应该一样。

字符指针的特殊之处

以上操作实在没搞懂字符指针和普通的指向数组的指针有什么区别…

但字符指针可以这么操作:

1126111

1126112

如果字符指针有特殊之处,就在于它特殊的赋值方式…… 也就是第9-11行,右边写的是字符串常量却代表的是字符数组的指针。

另外还有的就是,由于这样:这些字符指针变量是指向字符串常量的指针,由于常量和变量的储存位置不同,地址要短很多。而还有的就是,两个指向”hello world!”的地址是一样的。

这些应该都是C语言实现的问题。

printf() 函数和字符指针

课本上说,下图第5行的语句中,printf 接受的是一个指向字符数组第一个字符的指针。也就是字符串常量可通过一个指向其第一个元素的指针访问。

好,我们看下以上的代码,pchar 是指向字符串常量第一个元素的指针,它的地址是 0x400671,然后我们可以通过 %s,pchar 来输出这之后的字符,直到遇到 ‘\0’。

所以,以上第9行和第10行应该是等价的,而第5行应该是它们的缩写。感觉上9、10行的写法更规范一些。

看地址学指针1:指针和数组

数组:

学习数组的时候老师强调,数组是在内存空间上连续分布的一群相同数据类型的元素的集合。图中,数组并没有初始化,但运行时仍然分配了内存

11251

11252

的确是这样,并且数组名 a (它的值是地址)和数组第0个元素 a[0] 地址是一样的。

 

指向数组的指针:

11253

11254

以上代码和运行结果就说明了,申明一个指针 pa 指向数组 a 后:

  1. pa + i 和 &a[i] 是等价的
  2. *(pa + i) 和 a[i] 是等价的

事实上,即使你让数组越界也不会有什么事情发现,地址输出仍然是正确的,只是值是不可预知的。如下代码所示:

11255

11256

指向数组的指针没什么特别的。如下,我们可以用指针变量 pa 作为条件判断依据:

11257

11258

 

数组名不是指针变量

数组名是数组首个元素的地址,但不是指针变量。它和指针变量是有区别的!如下:

11267

11268

11265

11266

1.pa 是个变量,可以 pa++ 而 a++ 是非法的。

2.sizeof(数组名)得到的是数组的大小,而sizeof(指针)得到的就是指针的大小啦!

总的来说,数组名更像是指针,而不是指针变量

 

混淆:指针和指针变量

老师说过,指针是指针变量的值。似乎还说过指针和地址是等价的。我们说指针变量是有类型的(那么指针也有类型?),虽然我不是很清楚指针和地址的更多区别,但为了避免混淆,我们还是尽量明确“指针变量”,感觉平时说的“指针”,很多时候是指“指针变量”…这里只是提一下。

 

PS:接下来这部分内容主要参考<The C Programming Language>(C程序设计语言-第2版)来写

这篇文章对应于 第83页 5.3 指针与数组

第5.4 地址算术运算,这部分我没看懂….先跳过了

看地址学指针0:指针的引入

指针的定义

22222

指针的输出(查看地址)

如何printf一个指针呢?指针的格式控制字符是 %p

嗯,从输出结果知道:我的64位(bit)机器上地址大小是8字节(byte)(8 byte = 64 bit)

O(∩_∩)O哈!新技能get√!!以后一定要多多输出指针,当搞不清楚指针到底指向哪里的时候,不妨输出地址看看嘛!

读地址

我们看上图中 x 的地址是 0x 开头的一串字符,也就是十六进制数。地址在计算机中是独一无二的,没有重复的地址,地址的大小在 [0, 2^64 – 1]范围内

2进制64位数字用16进制表示应该有16位,因为每相邻4位数可以等价于1位16进制位。而x的地址之所以不足16位是因为:它把前面的0省略了。我们还可以看到,地址0是(nil),地址1是0x1。

运算符的优先级:* 和 i++ 是一样的

首先,常用运算符的优先级是这样的:

我们知道 i++ 和 ++i 是不一样的:

i++

which means that the current value is used in the expression, and then it is incremented

++i

the current value is incremented first, and then it is used in the expression.

举例:

现在我们看下 * 的优先级:

以上的代码就能说明 *y++ 的执行顺序是从右到左的

这部分代码说明的问题还是很好玩的,我们来解读一下:

  1. 你可以发现两次输出的地址(y的值)差4, 因为int大小位4个字节,也就是 y++ 表示 y = y + 4,加的其实是地址的值
    y = 0x7ffff3405b2c
    y = 0x7ffff3405b30
    0x7ffff3405b2c + 4 = 0x7ffff3405b30
  2. 由于 x 被初始化为1,故 *y = 1;但是 y++之后,y指向x地址的后一个地址,它并没有初始化,故 *y 将不知道得到什么(也就是出现无可预知的值)
  3. (*z)等价于 x,然后我们发现第一次 y 的值和 z 的值是相等的,
    z = 0x7ffff3405b2c
    也就是 x = x + 1,只是把 x 的值加1,并没有改变 x 的地址。相当于往 x 的内存地址上,把原来的值擦掉,写上新的值
  4. *运算,i++,++i 优先级是一样的

 

好啦,最后还有一个新的发现:可执行文件每执行一次,内存地址就重新分配一次!

另外刚刚尝试了这个

* 和 & 互为逆运算

C语言标准库

对于程序员来说,库是最重要的工具之一,可以避免重新造轮子。

课堂上助教给我们展示了:标准库中关于printf( )函数的部分,于是代码刷屏了好几秒 ( ⊙o⊙ )

我们来看下这个wiki词条

  1. 定义:所有目前符合标准的头文件(head file)的集合,以及常用的函数库实现程序→C标准函数库
  2. 标准函数库通常会随附在编译器上。因为 C 编译器常会提供一些额外的非 ANSI C函数功能,所以某个随附在特定编译器上的标准函数库,对其他不同的编译器来说,是不兼容的。
  3. 字符串输入函数gets()(以及 scanf() 读取字符串输入的使用上)是很多缓存溢出的原因,而且大多的程序设计指南会建议避免使用它。
  4. 至此,C标准函数库共有29个头文件。目前我用到的有:<stdio.h>,<math.h>,<complex.h>,<stdlib.h>,<time.h>

  • 最常用的 stdio.h 头文件
  1. C语言为文件输入输出提供了许多标准库函数。这些库函数构成了C标准库头文件<stdio.h>的主体。
  2. 就像 printf() 函数的申明在头文件中,而函数的定义(实现)并不在里面
  3. 原来常量 EOF 和 NULL 都定义在这里,我最近才知道 NULL 相当于0,不是字符0,而是它的ASCII码值为0,并且与字符 ‘\0’ 是等价的
  4. 然后刚刚在ASCII码表上查不到 EOF 的值,它等于 -1。搜了一下又进入阮一峰的博客惹→EOF是什么

最后让我们看下<stdio.h>头文件吧~

  • 头文件与源文件

既然函数的定义不在头文件中,那又在哪呢?又为什么要把函数的申明和定义分开?

除了以 *.h 命名的头文件(header)以外,还有实现函数功能的源文件implementation(*.c),函数的定义就在里面。这么做是基于 information hiding(encapsulation/封装)的原则。

过程是这样,计算机先把 .c 和 .h 文件编译成 .o 文件,再把他们链接在一起,成为一个二进制文件。

而库的编写者只需要把库文件(binary)和头文件(header)给想要用这个库的人就行了

C语言头文件和源文件分开有很多优点,也是有不断发展最后成这样的。有兴趣可以看下这些文章(其实我是自己还没看先记下来…fix me!)

头文件与之实现文件的的关系~

C 语言项目中.h文件和.c文件的关系[转载]

C++:源文件与头文件有什么区别

  • 如何使用C语言库

Step1:include the header files

如果库在 usr/include/ 目录下,那么就用 #include < *.h >

如果库在当前目录下,就用 #include “mylib.h”

Step2:link in the libraries

也就是把你的代码和编译后的二进制库文件链接起来,否则会出现 ”Undefined symbols”

一般 gcc 会自动帮你链接,而简化你的编译操作。但有些情况你还是要自己链接,比如<math.h> 库 (貌似最新的 gcc 已经可以自动链接了?)

输入这一行命令链接即可$: gcc hello.c -l[lib]

(可以尝试写一个…fix me!)

编译时发生了什么?

一、你知道编译的时候发生了什么吗?

你知道C语言编译时发生了什么吗?

是不是像我一样嫌弃老师上课讲的冗长又抽象,搞什么嘛!

说那么多还不如把细节展示给我看!

这周的Shorts视频中有一集叫做Compilers,7分钟不仅详细讲了编译时的四个步骤,还打开查看了 -E -S -c之后的文件。看完感到极大的满足,于是动手试了一遍,下面把视频转换成图文,跟我一起学C语言啦!

首先,写一个大家非常熟悉的”hello world!”代码

哦,我用的编辑器是emacs,操作系统是ubuntu(linux),然后直接在终端(Terminal)下用gcc编译,然后 ./a.out 执行。运行正常

这就完成了一次编译到运行的操作。平时我做作业也都是这样,并没有理会从C语言到机器语言的编译过程具体是什么样。

二、编译的四个步骤

gcc的编译流程分为四个步骤,分别为:

・ 预处理(Pre-Processing)
・ 编译(Compiling)
・ 汇编(Assembling)
・ 链接(Linking)

(1)编译预处理(Pre-Processing)

先输入这一行命令:gcc -E 目标文件.c

回车后就被刷屏了….

把这些代码导入到一个hello2.c的文件中

打开后

这是什么鬼!长这么奇怪= =

哈,居然有800多行,拉到最后终于看到熟悉的 “hello world!”代码片段惹!

这就是编译预处理啦~ 搜了一下什么是编译预处理

【摘】预编译的主要作用如下:

●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。

是嘛~刚刚刷屏那个就是把源代码中 #include <stdio.h> 这一行替换成了834行代码..

再拿宏定义试试,注意第3行

相同操作之后,对比预处理之后的.c文件

喏,836行之前没有区别!只有原来 #define name “Jenny” 那一行消失了,取而代之的是源代码中 printf(“Hello %s\n”,name); 中的name 被换成了 ”Jenny”

这就是传说中的,define只做“文本替换”啦

(2)编译(Compiling)

下面用 gcc -S 目标文件.c 来编译,用C语言代码 生成 汇编语言

打开编译后的 hello.s 文件

又是什么鬼,完全看不懂….

(3)汇编(Assembling)

用 gcc -c 目标文件.s 把

打开 hello.o二进制文件

这就是传说中的机器码了,你看到中间的 “hello world!” 了吗?

*Bonus 直接修改二进制文件

让我们改改机器码试试!→_→

改成 no zuo no die ….

运行试试

玩坏了 ..>_<.. 一定是字节不一样!!

改成hello jenny

这样就行啦!

*Bonus 用十六进制看机器码!

哈哈哈,有点像传说中的10101010串了吧!我不知道怎么通过二进制查看机器码….

再改成nozuonodie!!

这次成功了

(4) 链接(Linking)

刚刚一直到这个步骤,其实代码都不能执行。可执行文件 a.out 是我之前一步gcc hello.c生成的。

最后一步链接,需要在二进制文件 hello.o 的基础上,生成可执行文件 .out

命令为 gcc hello.o

列出当前文件夹下面的文件,可以发现只有 hello.out 是绿色的,并且最左边一堆rwx里面只有hello.out带有x,那些rwx表示的是”read”,”write”,”execute”的权限啦!