开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

MyFlash——美团点评的开源MySQL闪回工具 闪回工具

发表于 2017-11-17

由于运维、DBA的误操作或是业务bug,我们在操作中时不时会出现误删除数据情况。早期要想恢复数据,只能让业务人员根据线上操作日志,构造误删除的数据,或者DBA使用binlog和备份的方式恢复数据,不管那种,都非常费时费力,而且容易出错。直到彭立勋首次在MySQL社区为mysqlbinlog扩展了闪回功能。

在美团点评,我们也遇到过研发人员误删主站的配置信息,从而导致主站长达2个小时不可用的情况。DBA同学当时使用了技术团队自研的binlog2sql完成了数据恢复,并多次挽救了线上误删数据导致的严重故障。不过,binlog2sql在恢复速度上不尽如人意,因此我们开发了一个新的工具——MyFlash,它很好地解决了上述痛点,能够方便并且高效地进行数据恢复。

现在该工具正式开源,开源地址为:github.com/Meituan-Dia… 。

闪回工具现状

先来看下目前市面上已有的恢复工具,我们从实现角度把它们划分成如下几类。

① mysqlbinlog工具配合sed、awk。该方式先将binlog解析成类SQL的文本,然后使用sed、awk把类SQL文本转换成真正的SQL。

  • 优点:当SQL中字段类型比较简单时,可以快速生成需要的SQL,且编程门槛也比较低。
  • 缺点:当SQL中字段类型比较复杂时,尤其是字段中的文本包含HTML代码,用awk、sed等工具时,就需要考虑极其复杂的转义等情况,出错概率很大。

② 给数据库源码打patch。该方式扩展了mysqlbinlog的功能,增加Flashback选项。

  • 优点:复用了MySQL Server层中binlog解析等代码,一旦稳定之后,无须关心复杂的字段类型,且效率较高。
  • 缺点:在修改前,需要对MySQL的复制代码结构和细节需要较深的了解。版本比较敏感,在MySQL 5.6上做的patch,基本不能用于MySQL 5.7的回滚操作。升级困难,因为patch的代码是分布在MySQL的各个文件和函数中,一旦MySQL代码改变,特别是复制层的重构,升级的难度不亚于完全重新写一个。

③ 使用业界提供的解析binlog的库,然后进行SQL构造,其优秀代表是binlog2sql。

  • 优点:使用业界成熟的库,因此稳定性较好,且上手难度较低。
  • 缺点:效率往往较低,且实现上受制于binlog库提供的功能。

上述几种实现方式,主要是提供的过滤选项较少,比如不能提供基于SQL类型的过滤,需要回滚一个delete语句,导致在回滚时,需要结合awk、sed等工具进行筛选。

总结了上述几种工具的优缺点,我认为理想的闪回工具需要有以下特性。

a. 无需把binlog解析成文本,再进行转换。
b. 提供原生的基于库、表、SQL类型、位置、时间等多种过滤方式。
c. 支持MySQL多个版本。
d. 对于数据库的代码重构不敏感,利于升级。
e. 自主掌控binlog解析,提供尽可能灵活的方式。

在这些特性中,binlog的解析是一切工作的基础。接下来我会介绍binlog的基本结构。

binlog格式初探

binlog格式概览

一个完整的binlog文件是由一个format description event开头,一个rotate event结尾,中间由多个其他event组合而成。

binlog文件实例:

每个event都是由event header 和event data组成。下面简单介绍下几种常见的binlog event。

① formart description event

表达的含义是:

1
2
复制代码170905  01:59:33 server id 10  end_log_pos 123 CRC32 0xed1ec563 
Start: binlog v 4, server v 5.7.18-log created 170905 01:59:33

② table map event

表达的含义是:

1
2
复制代码    170905  01:59:33 server id 10  end_log_pos 339 CRC32 0x3de40c0d     
Table_map: `test`.`test4` mapped to number 238

③ update row event

表达的含义是:

1
2
3
复制代码    170905  01:59:33 server id 10  end_log_pos 385 CRC32 0x179ef6dd     
Update_rows: table id 238 flags: STMT_END_F
UPDATE `test`.`test4` WHERE @1=3 SET @1=13;

binlog event回滚

根据上面的binlog介绍,可以看到每个binlog event中event header有个type_code,其中insert为30,update为31,delete为32。对于insert和delete两个相反的操作,只需把type_code互换,则在binlog event级别完成回滚。

而对于update操作,其格式如下。

其中,BI是指before image,AI是指after image。

我们只需依次遍历修改前的数据和修改后的数据,并一一互换即可。因此整个回滚操作的难点在于回滚update语句,而update语句回滚的核心在于计算出每个AI、BI的长度。下面介绍下长度以及部分字段的计算方法。

镜像长度计算

镜像是由一个个字段组成的,根据字段类型的不同,其计算长度的方法也不一样。

  • 只与字段类型相关。比如int占用4个字节,bingint占用8个字节。其中类型信息可以从table map event中获取。
  • 与字段类型及其参数相关。比如decimal(18,9),占用9个字节,参数信息在table map event中。
  • 与字段类型、参数以及实际存储的值相关。比如varchar(10),有1个字节表示长度,之后的字节才表示真正的数据。比如varchar(280),有2个字节表示长度。实际的长度和数据在一起。

解析binlog中的若干个关键点

① length encoded integer

binlog中一个或者多个字节组合,分别表示了不同的含义。比如,timestamp是由固定的4个字节组成,event类型由一个字节表示;数据库名和表名最长为64个字符,即使每个字符占用3个字节,那么占用的字节数为192<255。因此最多使用一个字节,就可以完成实际长度表示。

然而列的实际数量,可能需要超过1个字节、2个字节、3个字节甚至8个字节去表示。如果我们使用最大的8个字节去表示,那么在绝大多数情况下都是浪费存储空间的。针对这种情况,length encoded integer应运而生。

比如在获取一个varchar类型的长度时,首先读取第一个字节,如果值小于251,那么varchar的长度就是第一个字节表示的长度。如果第一个字节的值为0xFC,那么varchar的长度是由该字节之后的后两个字节组成,以此类推。

② decimal类型

decimal是由整数部分和小数部分组成。无论是整数还是小数,每9个数字,需要4个字节。如果不是9的倍数,剩余的小数位,需要的字节数如下,为方便描述,将该关系定义为函数Fnum。

举例,对于 decimal(18,10):

  1. 整数部分可展示的为8,用int,即4个字节。
  2. 小数部分,需要的字节数为 (10 /9)*4+Fnum(10%9)=5。
  3. 那么总共加起来需要4+5=9个字节。

闪回工具架构

在上面的章节中,介绍了单个binlog event的反转方法。在实践中,我们往往需要把某个binlog,按照指定的条件,过滤出需要的binlog,并进行反转。那么MyFlash是如何完成这些目标的呢?

解析binlog

首先把binlog文件,解析成多个event,放入到相关队列中。在实现上,为了尽可能加快解析速度,可以让用户指定解析的开始与结束位置。把binlog文件解析成binlog event后,再判断下是否符合指定的时间条件,若不符合,则丢弃该event。

注意:用户可以不指定位置和时间,则解析整个文件。如果只指定时间,那么也需要从文件开始处解析,取出时间信息,再进行判断。因此,当需要回滚的binlog只占整个binlog的一小部分时,推荐使用指定位置。

重组event

把binlog event组成最小执行单元。在常见的binlog event中table_map event包含了所要了表名、库名等元数据信息,而row_event(包含write_event、delete_event、update_event)包含了真正的数据。因此在设计中使用了一个最小执行单元概念。所谓的最小执行单元,即least execution event unit,通常包含一个table_map event和若干个row_event。

比如在binlog格式概览一节中,介绍了table_map_event和update_row_event。如果只有update_row_event,那么我们无法知道这个event对应的行记录变更对应的表。因此一个完整的最小执行单元最少包含一个table_map_event和write_row_event、update_row_even、delete_row_event中的一个。

为什么我们需要使用最小执行单元?因为我们在闪回操作时,不能简单的把每个event反转之后,然后再将所有event的顺序反转过来。如果这样的话,就会出现table_map event在row event之后,这显然是违反binlog执行逻辑的。

有了最小执行单元之后,只需两步,即可完成反转。

a. 反转最小执行单元中的row event。
b. 逆序最小执行单元队列,即可。

当然在反转前,也可以增加过滤操作。比如过滤库名、表名和SQL类型等。

生成binlog文件

有了逆序的最小执行单元队列后,只需把每个最小执行单元依次输入到文件即可。不过不要忘了修改每个binlog event里的next_position,用来表示下一个binlog的位置。

性能对比

测试场景

使用testFlashback2,插入100万条数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码CREATE TABLE `testFlashback2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nameShort` varchar(20) DEFAULT NULL,
`nameLong` varchar(260) DEFAULT NULL,
`amount` decimal(19,9) DEFAULT NULL,
`amountFloat` float DEFAULT NULL,
`amountDouble` double DEFAULT NULL,
`createDatetime6` datetime(6) DEFAULT NULL,
`createDatetime` datetime DEFAULT NULL,
`createTimestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`nameText` text,
`nameBlob` blob,
`nameMedium` mediumtext,
PRIMARY KEY (`id`)
) ENGINE=InnoDB

mysql> select count(*) from testFlashback2;
+----------+
| count(*) |
+----------+
| 1048576 |
+----------+
1 row in set (0.16 sec)

delete from testFlashback2;

测试结果

从上述图表中可以看出,MyFlash的速度最快。

参考文档

  1. MySQL官方文档1,2,3.
  2. binlog2sql.
  3. mysqlbinlog Flashback for 5.6.
  4. MySQL闪回原理与实战.

招聘

美团点评DBA团队招聘各类DBA人才,base北京上海均可。我们致力于为公司提供稳定、可靠、高效的在线存储服务,打造业界领先的数据库团队。这里有基于Redis Cluster构建的大规模分布式缓存系统Squirrel,也有基于Tair进行大刀阔斧改进的分布式KV存储系统Cellar,还有数千各类架构的MySQL实例,每天提供万亿级的OLTP访问请求。真正的海量、分布式、高并发环境。欢迎各位朋友推荐或自荐至jinlong.cai#dianping.com。

回答“思考题”、发现文章有错误、对内容有疑问,都可以来微信公众号(美团点评技术团队)后台给我们留言。我们每周会挑选出一位“优秀回答者”,赠送一份精美的小礼品。快来扫码关注我们吧!

公众号二维码

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用普通的Python,从头开始编写一个基本的x86-64 J

发表于 2017-11-17

Writing a basic x86-64 JIT compiler from scratch in stock Python

By Christian Stigen Larsen
Posted 08 Nov 2017 — updated 11 Nov 2017

In this post I’ll show how to write a rudimentary, native x86-64 just-in-time
compiler (JIT)
in CPython, using only the built-in modules.

Update: This post made the front page of HN, and I’ve incorporated some of the discussion feedback. I’ve also written a
follow-up post that JITs Python bytecode to x86-64.

The code here specifically targets the UNIX systems macOS and Linux, but should be easily translated to other systems such as Windows. The complete code is available on github.com/cslarsen/mi….

The goal is to generate new versions of the below assembly code at runtime and execute it.

1
2
3
4
复制代码48 b8 ed ef be ad de  movabs $0xdeadbeefed, %rax
00 00 00
48 0f af c7 imul %rdi,%rax
c3 retq

We will mainly deal with the left hand side — the byte sequence 48 b8 ed ... and so on. Those fifteen machine code bytes comprise an x86-64 function that multiplies its argument
with the constant 0xdeadbeefed. The JIT step will create functions with different such constants. While being a contrived form of specialization, it illuminates
the basic mechanics of just-in-time compilation.

Our general strategy is to rely on the built-in ctypes Python module to load the C standard library. From there, we can access system functions to interface with the virtual memory manager. We’ll use mmap to fetch a page-aligned
block of memory. It needs to be aligned for it to become executable. That’s the reason why we can’t simply use the usual C function malloc, because it may return memory that spans page boundaries.

The function mprotect will be used to mark the memory block as read-only and executable. After that, we should be able to call into our freshly compiled block of code through ctypes.

The boiler-plate part

Before we can do anything, we need to load the standard C library.

1
2
3
4
5
6
7
8
9
10
11
复制代码import ctypes
import sys

if sys.platform.startswith("darwin"):
libc = ctypes.cdll.LoadLibrary("libc.dylib")
# ...
elif sys.platform.startswith("linux"):
libc = ctypes.cdll.LoadLibrary("libc.so.6")
# ...
else:
raise RuntimeError("Unsupported platform")

There are other ways to achieve this, for example

1
2
3
4
5
复制代码>>> import ctypes
>>> import ctypes.util
>>> libc = ctypes.CDLL(ctypes.util.find_library("c"))
>>> libc
<CDLL '/usr/lib/libc.dylib', handle 110d466f0 at 103725ad0>

To find the page size, we’ll call sysconf(_SC_PAGESIZE). The _SC_PAGESIZE constant is 29 on macOS but 30 on Linux. We’ll just hard-code those in our program. You can find them by digging into system header files or writing
a simple C program that print them out. A more robust and elegant solution would be to use the cffi module instead of ctypes, because it can automatically parse header files. However, since I wanted to stick to the default CPython
distribution, we’ll continue using ctypes.

We need a few additional constants for mmap and friends. They’re just written out below. You may have to look them up for other UNIX variants.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码import ctypes
import sys

if sys.platform.startswith("darwin"):
libc = ctypes.cdll.LoadLibrary("libc.dylib")
_SC_PAGESIZE = 29
MAP_ANONYMOUS = 0x1000
MAP_PRIVATE = 0x0002
PROT_EXEC = 0x04
PROT_NONE = 0x00
PROT_READ = 0x01
PROT_WRITE = 0x02
MAP_FAILED = -1 # voidptr actually
elif sys.platform.startswith("linux"):
libc = ctypes.cdll.LoadLibrary("libc.so.6")
_SC_PAGESIZE = 30
MAP_ANONYMOUS = 0x20
MAP_PRIVATE = 0x0002
PROT_EXEC = 0x04
PROT_NONE = 0x00
PROT_READ = 0x01
PROT_WRITE = 0x02
MAP_FAILED = -1 # voidptr actually
else:
raise RuntimeError("Unsupported platform")

Although not strictly required, it is very useful to tell ctypes the signature of the functions we’ll use. That way, we’ll get exceptions if we mix invalid types. For example

1
2
3
4
复制代码# Set up sysconf
sysconf = libc.sysconf
sysconf.argtypes = [ctypes.c_int]
sysconf.restype = ctypes.c_long

tells ctypes that sysconf is a function that takes a single integer and produces a long integer. After this, we can get the current page size with

1
复制代码pagesize = sysconf(_SC_PAGESIZE)

The machine code we are going to generate will be interpreted as unsigned 8-bit bytes, so we need to declare a new pointer type:

1
2
复制代码# 8-bit unsigned pointer type
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)

Below we just dish out the remaining signatures for the functions that we’ll use. For error reporting, it’s good to have the strerror function available. We’ll use munmap to destroy the machine code block after we’re done
with it. It lets the operating system reclaim that memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码strerror = libc.strerror
strerror.argtypes = [ctypes.c_int]
strerror.restype = ctypes.c_char_p

mmap = libc.mmap
mmap.argtypes = [ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
# Below is actually off_t, which is 64-bit on macOS
ctypes.c_int64]
mmap.restype = c_uint8_p

munmap = libc.munmap
munmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
munmap.restype = ctypes.c_int

mprotect = libc.mprotect
mprotect.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int]
mprotect.restype = ctypes.c_int

At this point, it’s hard to justify using Python rather than C. With C, we don’t need any of the above boiler-plate code. But down the line, Python will allow us to experiment much more easily.

Now we’re ready to write the mmap wrapper.

1
2
3
4
5
6
7
8
复制代码def create_block(size):
ptr = mmap(0, size, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)

if ptr == MAP_FAILED:
raise RuntimeError(strerror(ctypes.get_errno()))

return ptr

This function uses mmap to allocate page-aligned memory for us. We mark the memory region as readable and writable with the PROT flags, and we also mark it as private and anonymous. The latter means the memory will not be
visible from other processes and that it will not be file-backed. The Linux mmap manual page covers the details (but be sure to view the man page for your system). If the mmap call fails, we raise it as a Python error.

To mark memory as executable,

1
2
3
复制代码def make_executable(block, size):
if mprotect(block, size, PROT_READ | PROT_EXEC) != 0:
raise RuntimeError(strerror(ctypes.get_errno()))

With this mprotect call, we mark the region as readable and executable. If we wanted to, we could have made it writable as well, but some systems will refuse to execute writable memory. This is sometimes called the W^X security
feature
.

To destroy the memory block, we’ll use

1
2
3
复制代码def destroy_block(block, size):
if munmap(block, size) == -1:
raise RuntimeError(strerror(ctypes.get_errno()))

I edited out a badly placed del in that function after the HN submission.

The fun part

Now we’re finally ready to create an insanely simple piece of JIT code!

Recall the assembly listing at the top: It’s a small function — without a local stack frame — that multiplies an input number with a constant. In Python, we’d write that as

1
2
复制代码def create_multiplication_function(constant):
return lambda n: n * constant

This is indeed a contrived example, but qualifies as JIT. After all, we do create native code at runtime and execute it. It’s easy to imagine more advanced examples such as JIT-compiling Brainfuck to x86-64 machine code. Or using AVX instructions for blazing fast, vectorized math ops.

The disassembly at the top of this post was actually generated by compiling and disassembling the following C code:

1
2
3
4
5
6
复制代码#include <stdint.h>

uint64_t multiply(uint64_t n)
{
return n*0xdeadbeefedULL;
}

If you want to compile it yourself, use something like

1
2
复制代码$ gcc -Os -fPIC -shared -fomit-frame-pointer \
-march=native multiply.c -olibmultiply.so

Here I optimized for space (-Os) to generate as little machine code as possible, with position-independent code (-fPIC) to prevent using absolute jumps, without any frame pointers (-fomit-frame-pointer) to remove
superfluous stack setup code (but it may be required for more advanced functions) and using the current CPU’s native instruction set (-march=native).

We could have passed -S to produce a disassembly listing, but we’re actually interested in the machine code, so we’ll rather use a tool like objdump:

1
2
3
4
5
6
7
复制代码$ objdump -d libmultiply.so
...
0000000000000f71 <_multiply>:
f71: 48 b8 ed ef be ad de movabs $0xdeadbeefed,%rax
f78: 00 00 00
f7b: 48 0f af c7 imul %rdi,%rax
f7f: c3 retq

In case you are not familiar with assembly, I’ll let you know how this function works. First, the movabs function just puts an immediate number in the RAX register. Immediate is assembly-jargon for encoding something
right in the machine code. In other words, it’s an embedded argument for the movabs instruction. So RAX now holds the constant 0xdeadbeefed.

Also — by AMD64 convention — the first integer argument will be in RDI, and the return value in RAX. So RDI will hold the number to multiply with. That’s
what imul does. It multiplies RAX and RDI and puts the result in RAX. Finally, we pop a 64-bit return address off the stack and jump to it with RETQ. At this level, it’s easy to imagine how one could implement continuation-passing style.

Note that the constant 0xdeadbeefed is in little-endian format. We need to remember to do the same when we patch the code. (By the way, a good mnemonic for remembering the word order is that little endian means “little-end first”).

We are now ready to put everything in a Python function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码def make_multiplier(block, multiplier):
# Encoding of: movabs <multiplier>, rax
block[0] = 0x48
block[1] = 0xb8

# Little-endian encoding of multiplication constant
block[2] = (multiplier & 0x00000000000000ff) >> 0
block[3] = (multiplier & 0x000000000000ff00) >> 8
block[4] = (multiplier & 0x0000000000ff0000) >> 16
block[5] = (multiplier & 0x00000000ff000000) >> 24
block[6] = (multiplier & 0x000000ff00000000) >> 32
block[7] = (multiplier & 0x0000ff0000000000) >> 40
block[8] = (multiplier & 0x00ff000000000000) >> 48
block[9] = (multiplier & 0xff00000000000000) >> 56

# Encoding of: imul rdi, rax
block[10] = 0x48
block[11] = 0x0f
block[12] = 0xaf
block[13] = 0xc7

# Encoding of: retq
block[14] = 0xc3

# Return a ctypes function with the right prototype
function = ctypes.CFUNCTYPE(ctypes.c_uint64)
function.restype = ctypes.c_uint64
return function

At the bottom, we return the ctypes function signature to be used with this code. It’s somewhat arbitrarily placed, but I thought it was good to keep the signature close to the machine code.

The final part

Now that we have the basic parts we can weave everything together. The first part is to allocate one page of memory:

1
2
复制代码pagesize = sysconf(_SC_PAGESIZE)
block = create_block(pagesize)

Next, we generate the machine code. Let’s pick the number 101 to use as a multiplier.

1
复制代码mul101_signature = make_multiplier(block, 101)

We now mark the memory region as executable and read-only:

1
复制代码make_executable(block, pagesize)

Take the address of the first byte in the memory block and cast it to a callable ctypes function with proper signature:

1
2
复制代码address = ctypes.cast(block, ctypes.c_void_p).value
mul101 = mul101_signature(address)

To get the memory address of the block, we use ctypes to cast it to a void pointer and extract its value. Finally, we instantiate an actual function from this address using the mul101_signature constructor.

Voila! We now have a piece of native code that we can call from Python. If you’re in a REPL, you can try it directly:

1
2
复制代码>>> print(mul101(8))
808

Note that this small multiplication function will run slower than a native Python calculation. That’s mainly because ctypes, being a foreign-function library, has a lot of overhead: It needs to inspect what dynamic types you pass the function every
time you call it, then unbox them, convert them and then do the same with the return value. So the trick is to either use assembly because you have to access some new Intel instruction, or because you compile something like Brainfuck to native
code.

Finally, if you want to, you can let the system reclaim the memory holding the function. Beware that after this, you will probably crash the process if you try calling the code again. So probably best to delete all references in Python as well:

1
2
3
4
复制代码destroy_block(block, pagesize)

del block
del mul101

If you run the code in its complete form from the GitHub repository, you can put the multiplication constant on the command line:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码$ python mj.py 101
Pagesize: 4096
Allocating one page of memory
JIT-compiling a native mul-function w/arg 101
Making function block executable
Testing function
OK mul(0) = 0
OK mul(1) = 101
OK mul(2) = 202
OK mul(3) = 303
OK mul(4) = 404
OK mul(5) = 505
OK mul(6) = 606
OK mul(7) = 707
OK mul(8) = 808
OK mul(9) = 909
Deallocating function

Debugging JIT-code

If you want to continue learning with this simple program, you’ll quickly want to disassemble the machine code you generate. One option is to simply use gdb or lldb, but you need to know where to break. One trick is to just print the hex value of
the block address and then wait for a keystroke:

1
2
3
复制代码print("address: 0x%x" % address)
print("Press ENTER to continue")
raw_input()

Then you just run the program in the debugger, break into the debugger while the program is pausing, and disassemble the memory location. Of course you can also step-debug through the assembly code if you want to see what’s going on. Here’s an example
lldb session:

1
2
3
4
5
6
7
8
9
复制代码$ lldb python
...
(lldb) run mj.py 101
...
(lldb) c
Process 19329 resuming
...
address 0x1002fd000
Press ENTER to continue

At this point, hit CTRL+C to break back into the debugger, then disassemble from the memory location:

1
2
3
4
复制代码(lldb) x/3i 0x1002fd000
0x1002fd000: 48 b8 65 00 00 00 00 00 00 00 movabsq $0x65, %rax
0x1002fd00a: 48 0f af c7 imulq %rdi, %rax
0x1002fd00e: c3 retq

Notice that 65 hex is 101 in decimal, which was the command line argument we passed above.

If you only want a disassembler inside Python, I recommend the Capstone module.

What’s next?

A good exercise would be to JIT-compile Brainfuck programs to native code. If you want to jump right in, I’ve made a GitHub repository at github.com/cslarsen/br….
I even have a Speaker Deck presentation to go with it. It performs JIT-ing and optimizations, but uses GNU Lightning to compile native code instead of this approach.
It should be extremely simple to boot out GNU Lightning in favor or some code generation of your own. An interesting note on the Brainfuck project is that if you just JIT-compile each Brainfuck instruction one-by-one, you won’t get much of a speed
boost, even if you run native code. The entire speed boost is done in the code optimization stage, where you can bulk up integer operations into one or a few x86 instructions. Another candidate for such compilation would be the Forth language.

Also, before you get serious about expanding this JIT-compiler, take a look at the PeachPy project. It goes way beyond this and includes a disassembler and supports seemingly the entire x86-64 instruction
set right up to AVX.

As mentioned, there is a good deal of overhad when using ctypes to call into functions. You can use the cffi module to overcome some of this, but the fact remains that if you want to call very small JIT-ed functions a large number of
times, it’s usually faster to just use pure Python.

What other cool uses are there? I’ve seen some math libraries in Python that switch to vector operations for higher performance. But I can imagine other fun things as well. For example, tools to compress and decompress native code, access virtualization
primitives, sign code and so on. I do know that some BPF tools and regex modules JIT-compile queries for faster processing.

What I think is fun about this exercise is to get into deeper territory than pure assembly. One thing that comes to mind is how different instructions are disassembled to the same mnemonic. For example, the RETQ instruction has a different opcode
than an ordinary RET, because it operates on 64-bit values. This is something that may not be important when doing assembly programming, because it’s a detail that may not always matter, but it’s worth being aware of the difference. I saw that
gcc, lldb and objdump gave slightly different disassembly listings of the same code for RETQ and MOVABSQ.

There’s another takeaway. I’ve mentioned that the native Brainfuck compiler I made didn’t initially produce very fast code. I had to optimize to get it fast. So things won’t go fast just because you use AVX, Cuda or whatever. The cold truth is that
gcc contains a vast database of optimizations that you cannot possibly replicate by hand. Felix von Letiner has a classic talk about source
code optimization
that I recommend for more on this.

What about actual compilation?

A few people commented that they had expected to see more about the actual compilation step. Fair point. As it stands, this is indeed a very restricted form of compilation, where
we barely do anything with the code at runtime — we just patch in a constant. I may write a follow-up post that focuses solely on the compilation stage. Stay tuned!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Python、Neo4j和GraphQL,爬取俄罗斯Tw

发表于 2017-11-17

Scraping Russian Twitter Trolls With Python, Neo4j, and GraphQL

12 Nov 2017

Last week as a result of the House Intelligence Select Committee investigation, Twitter released the screen names of 2752 Twitter accounts tied to Russia’s Internet Research Agency that were involved in spreading fake news, presumably with the goal of influencing the 2016 election. In this post we explore how to scrape tweets from the user pages of cached versions of these pages, import into Neo4j for analysis, and how to
build a simple GraphQL API exposing this data through GraphQL.

Russian Twitter Trolls

While Twitter released the screen names and user ids of these accounts, they did not release any data (such as tweets or follower network information) associated with the accounts. In fact, Twitter has suspended these accounts which means their tweets
have been removed from Twitter.com and are no longer accessible through the Twitter API. Analyzing the tweets made by these accounts is the first step in understanding how social media accounts run by Russia may have been used to influence the
US Election. So our first step is simply to find potential sources for the data.

Internet Archive

Internet Archive is a non-profit library that provides cached version of some websites: a snapshot of a webpage at a given point in time that can be viewed later. One option for obtaining some of the Russian Troll
tweets is by using Internet Archive to find any Twitter user pages that may have been cached by Internet Archive.

For example, if you visit web.archive.org/web/2017081… we can see the Twitter page for @TEN_GOP, one of the Russia Troll accounts
that was designed to look like an account associated with the Tennessee Republican party.

This snapshot page contains several of @TEN_GOP’s most recent tweets (before the snapshot was taken by Internet Archive).

Finding Available Cached Pages

Using the screen names provided by the House Intelligence Committee we can use Internet Archive’s Wayback API to see if the user’s Twitter profile page was cached by Internet Archive at any point in time. We’ll write a simple Python script to iterate
through the list of Russian Troll twitter accounts, checking the Wayback API for any available cached pages.

We can do this by making a request to http://archive.org/wayback/available?url=http://twitter.com/TWITTER_SCREEN_NAME_HERE. This will return the url and timestamp of any caches made, if they exist. So we iterate through the list of twitter
screen names, checking the Wayback API for any available caches.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码import requests
items = []
initial = "http://archive.org/wayback/available"
# iterate through list of flagged twitter screen names
with open('./data/twitter_handle_urls.csv') as f:
for line in f:
params = {'url': line}
r = requests.get(initial, params=params)
d = r.json()
#print(d)
items.append(d)
# write URL of any available archives to file
with open('./data/avail_urls.txt', 'w') as f:
for item in items:
if 'archived_snapshots' in item:
if 'closest' in item['archived_snapshots']:
f.write(item['archived_snapshots']['closest']['url'] + '\n')

With this, we end up with a file twitter_handle_urls.csv that contains a list of Internet Archive urls for any of the Russian troll accounts that were archived by Internet Archive. Unfortunately, we only find just over 100 Russia Troll
accounts that were cached by Internet Archive. This is just a tiny sample of the overall accounts, but we should still be able to scrape tweets for these 100 users.

Scraping Twitter Profile Pages

Now, we’re ready to scrape the HTML from the Internet Archive caches to extract all the tweet content that we can.

We’ll make use of the BeautifulSoup Python package to help us extract the tweet data from the HTML. First, we’ll use Chrome devtools to inspect the structure of the HTML, seeing what elements contain the data we’re looking for:

Since the caches were taken at different times, the structure of the HTML may have changed. We’ll need to write code that can handle parsing these different formats. We’ve found two versions of the Twitter user pages in the caches. One from ~2015,
and one used around ~2016-2017.

Here is the code for scraping the data for one of the versions. The full code is available here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码import urllib
from bs4 import BeautifulSoup
import csv
import requests

# testing for new version

url = "http://web.archive.org/web/20150603004258/https://twitter.com/AlwaysHungryBae"
page = requests.get(url).text
soup = BeautifulSoup(page, 'html.parser')

tweets = soup.find_all('li', attrs={'data-item-type': 'tweet'})

for t in tweets:
tweet_obj = {}
tweet_obj['tweet_id'] = t.get("data-item-id")
tweet_container = t.find('div', attrs={'class': 'tweet'})
tweet_obj['screen_name'] = tweet_container.get('data-screen-name')
tweet_obj['permalink'] = tweet_container.get('data-permalink-path')
tweet_content = tweet_container.find('p', attrs={'class': 'tweet-text'})
tweet_obj['tweet_text'] = tweet_content.text
tweet_obj['user_id'] = tweet_container.get('data-user-id')

tweet_time = tweet_container.find('span', attrs={'class': '_timestamp'})
tweet_obj['timestamp'] = tweet_time.get('data-time-ms')

hashtags = tweet_container.find_all('a', attrs={'class': 'twitter-hashtag'})
tweet_obj['hashtags'] = []
tweet_obj['links'] = []

for ht in hashtags:
ht_obj = {}
ht_obj['tag'] = ht.find('b').text
ht_obj['archived_url'] = ht.get('href')
tweet_obj['hashtags'].append(ht_obj)

links = tweet_container.find_all('a', attrs={'class': 'twitter-timeline-link'})
for li in links:
li_obj = {}
if li.get('data-expanded-url'):
li_obj['url'] = li.get('data-expanded-url')
elif li.get('data-resolved-url-large'):
li_obj['url'] = li.get('data-resolved-url-large')
else:
li_obj['url'] = li.text
li_obj['archived_url'] = li.get('href')
tweet_obj['links'].append(li_obj)

print(tweet_obj)

BeautifulSoup allows us to select HTML elements by specifying attributes to match against. By inspecting the structure of the HTML page we can see which bits of the tweets are stored in different HTML elements so we know which to grab with BeautifulSoup.
We build up an array of tweet objects as we parse all the tweets on the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码
{
'tweet_id': '561931644785811457',
'screen_name': 'AlwaysHungryBae',
'permalink': '/AlwaysHungryBae/status/561931644785811457',
'tweet_text': 'Happy Super Bowl Sunday \n#superbowlfood pic.twitter.com/s6rwMtdLom',
'user_id': '2882130846',
'timestamp': '1422809918000',
'hashtags': [
{'tag': 'superbowlfood',
'archived_url': '/web/20150603004258/https://twitter.com/hashtag/superbowlfood?src=hash'
}
],
'links': [
{'url': 'pic.twitter.com/s6rwMtdLom',
'archived_url': 'http://web.archive.org/web/20150603004258/http://t.co/s6rwMtdLom'
},
{'url': 'https://pbs.twimg.com/media/B8xh2fFCQAE-vxU.jpg:large', '
archived_url': '//web.archive.org/web/20150603004258/https://twitter.com/AlwaysHungryBae/status/561931644785811457/photo/1'
}
]
}

Once we’ve extracted the tweets we write them to a json file:

1
2
3
4
复制代码# write tweets to file
import json
with open('./data/tweets_full.json', 'w') as f:
json.dump(tweet_arr, f, ensure_ascii=False, sort_keys=True, indent=4)

We end up finding about 1500 tweets from 187 Twitter accounts. This is only a fraction of the tweets sent by the Russian Trolls, but is still too much data for us to analyze by reading every tweet. We’ll make use of the Neo4j graph database to help
us make sense of the data. Using Neo4j we’ll be able to ask questions such as “What hashtags are used together most frequently?”, or “What are the domains of URLs shared in tweets that mention Trump?”.

Importing Into Neo4j

Now that we have our scraped tweet data we’re ready to insert into Neo4j. We have several options for importing data into Neo4j. We’ll do our import by loading the JSON data and
passing it as a parameter to a Cypher query, using the Python driver for Neo4j.

We’ll use a simple graph data model, treating Hashtags and Links as nodes in the graph, as well as the Tweet and User who posted the tweet.

Datamodel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码from neo4j.v1 import GraphDatabase
import json

driver = GraphDatabase.driver("bolt://localhost:7687")
with open('./data/tweets_full.json') as json_data:
tweetArr = json.load(json_data)

import_query = '''
WITH $tweetArr AS tweets
UNWIND tweets AS tweet
MERGE (u:User {user_id: tweet.user_id})
ON CREATE SET u.screen_name = tweet.screen_name
MERGE (t:Tweet {tweet_id: tweet.tweet_id})
ON CREATE SET t.text = tweet.tweet_text,
t.permalink = tweet.permalink
MERGE (u)-[:POSTED]->(t)

FOREACH (ht IN tweet.hashtags |
MERGE (h:Hashtag {tag: ht.tag })
ON CREATE SET h.archived_url = ht.archived_url
MERGE (t)-[:HAS_TAG]->(h)
)

FOREACH (link IN tweet.links |
MERGE (l:Link {url: link.url})
ON CREATE SET l.archived_url = link.archived_url
MERGE (t)-[:HAS_LINK]->(l)
)

'''

def add_tweets(tx):
tx.run(import_query, tweetArr=tweetArr)

with driver.session() as session:
session.write_transaction(add_tweets)

Graph Queries

Now that we have the data in Neo4j we can write queries to help make sense of what the Russian Trolls were tweeting about.

Interesting Queries

1
2
3
4
5
复制代码// Tweets for @TEN_GOP
MATCH (u:User)-[:POSTED]->(t:Tweet)-[:HAS_TAG]->(h:Hashtag)
WHERE u.screen_name = "TEN_GOP"
OPTIONAL MATCH (t)-[:HAS_LINK]->(l:Link)
RETURN *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码// What are the most common hashtags
MATCH (u:User)-[:POSTED]->(t:Tweet)-[:HAS_TAG]->(ht:Hashtag)
RETURN ht.tag AS hashtag, COUNT(*) AS num
ORDER BY num DESC LIMIT 10



╒══════════════════════╤═══════════╕
│"hashtag" │"num"│
╞══════════════════════╪═══════════╡
│"JugendmitMerkel" │90 │
├──────────────────────┼───────────┤
│"TagderJugend" │89 │
├──────────────────────┼───────────┤
│"politics" │61 │
├──────────────────────┼───────────┤
│"news" │30 │
├──────────────────────┼───────────┤
│"sports" │28 │
├──────────────────────┼───────────┤
│"Merkel" │26 │
├──────────────────────┼───────────┤
│"ColumbianChemicals" │25 │
├──────────────────────┼───────────┤
│"WorldElephantDay" │22 │
├──────────────────────┼───────────┤
│"crime" │21 │
├──────────────────────┼───────────┤
│"UnitedStatesIn3Words"│21 │
└──────────────────────┴───────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码// What hashtags are used together most frequently
MATCH (h1:Hashtag)<-[:HAS_TAG]-(t:Tweet)-[:HAS_TAG]->(h2:Hashtag)
WHERE id(h1) < id(h2)
RETURN h1.tag, h2.tag, COUNT(*) AS num ORDER BY num DESC LIMIT 15

╒═════════════════╤══════════════════╤═════╕
│"h1.tag" │"h2.tag" │"num"│
╞═════════════════╪══════════════════╪═════╡
│"JugendmitMerkel"│"TagderJugend" │89 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"WorldElephantDay"│22 │
├─────────────────┼──────────────────┼─────┤
│"JugendmitMerkel"│"WorldElephantDay"│22 │
├─────────────────┼──────────────────┼─────┤
│"JugendmitMerkel"│"Dschungelkönig" │21 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"Dschungelkönig" │21 │
├─────────────────┼──────────────────┼─────┤
│"Merkel" │"JugendmitMerkel" │17 │
├─────────────────┼──────────────────┼─────┤
│"Merkel" │"TagderJugend" │17 │
├─────────────────┼──────────────────┼─────┤
│"CDU" │"JugendmitMerkel" │12 │
├─────────────────┼──────────────────┼─────┤
│"CDU" │"TagderJugend" │12 │
├─────────────────┼──────────────────┼─────┤
│"TagderJugend" │"Thailand" │11 │
└─────────────────┴──────────────────┴─────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码// Most common domains shared in tweets
MATCH (t:Tweet)-[:HAS_LINK]->(u:Link)
WITH t, replace(replace(u.url, "http://", '' ), "https://", '') AS url
RETURN COUNT(t) AS num, head(split(url, "/")) ORDER BY num DESC LIMIT 10

╒═════╤═════════════════════════╕
│"num"│"head(split(url, \"/\"))"│
╞═════╪═════════════════════════╡
│835 │"pic.twitter.com" │
├─────┼─────────────────────────┤
│120 │"bit.ly" │
├─────┼─────────────────────────┤
│105 │"\n\n" │
├─────┼─────────────────────────┤
│100 │"pbs.twimg.com" │
├─────┼─────────────────────────┤
│32 │"vk.com" │
├─────┼─────────────────────────┤
│21 │"riafan.ru" │
├─────┼─────────────────────────┤
│21 │"inforeactor.ru" │
├─────┼─────────────────────────┤
│20 │"nevnov.ru" │
├─────┼─────────────────────────┤
│17 │"goodspb.livejournal.com"│
├─────┼─────────────────────────┤
│15 │"www.fox5atlanta.com" │
└─────┴─────────────────────────┘

GraphQL API

In addition to querying Neo4j using Cypher directly, we can also take advantage of the neo4j-graphql integrations to easily build a GraphQL API for our tweets.

First, we define a GraphQL schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码type Tweet {
tweet_id: ID!
text: String
permalink: String
author: User @relation(name: "POSTED", direction: "IN")
hashtags: [Hashtag] @relation(name: "HAS_TAG", direction: "OUT")
links: [Link] @relation(name: "HAS_LINK", direction: "OUT")
}

type User {
user_id: ID!
screen_name: String
tweets: [Tweet] @relation(name: "POSTED", direction: "OUT")
}

type Hashtag {
tag: ID!
archived_url: String
tweets(first: Int): [Tweet] @relation(name: "HAS_TAG", direction: "IN")
}

type Link {
url: ID!
archived_url: String


}


type Query {
Hashtag(tag: ID, first: Int, offset: Int): [Hashtag]
}

Our GraphQL schema defines the types and fields available in the data, as well as the entry points for our GraphQL service. In this case we have a single entry point Hashtag, allowing us to search for tweets by hashtag.

With the neo4j-graphql-js integration, the GraphQL schema maps to the graph database model and translates any arbitrary GraphQL query to Cypher, allowing anyone to query the data through
the GraphQL API without writing Cypher.

Implementing the GraphQL server is simply a matter of passing the GraphQL query to the integration function in the resolver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码import {neo4jgraphql} from 'neo4j-graphql-js';

const resolvers = {
// root entry point to GraphQL service
Query: {
// fetch movies by title substring
Hashtag(object, params, ctx, resolveInfo) {
// neo4jgraphql inspects the GraphQL query and schema to generate a single Cypher query
// to resolve the GraphQL query. Assuming a Neo4j driver instance exists in the context
// the query is executed against Neo4j
return neo4jgraphql(object, params, ctx, resolveInfo);
}
}
};

React App

One of the advantages of having a GraphQL API is that makes it very easy to build web and mobile applications that consume the GraphQL service. To make the data easily searchable we’ve make a simple React web app that allows for searching tweets in
Neo4j by hashtag.

Here we’re searching for tweets that contain the hashtag #crime. We can see that a Russia Troll account @OnlineCleveland is tweeting fake news about crimes in Ohio, making it seem that more crime is occurring in Cleveland. Why would a
Russia Troll account be tweeting about crime in Cleveland leading up to the election? Typically when voters want a “tough on crime” politician elected they vote Republican…

In this post we’ve scraped tweet data from Internet Archive, imported in Neo4j for analysis, built a GraphQL API for exposing the data, and a simple GRANDstack app for allowing anyone to easily search the tweets
by hashtag.

While we were only able to find a small fraction of the tweets posted by the Russian Twitter Troll accounts, we will continue to explore options for finding more of the data ;-)

All code is available on Github at github.com/johnymontan….

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 形参与实参

发表于 2017-11-17

前几天在头条上看到一道经典面试题,引发了一些思考。也是写这篇文章的导火索。

背景

请看题:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public	class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
//请实现
}
}

看到这个题后 瞬间觉得有坑。也觉得为什么要书写一个swap方法呢?如下实现不是更简单:

1
2
3
4
5
6
7
8
9
复制代码public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
System.out.println("a=" + a + ",b=" + b);
Integer tmp = a;
a = b;
b = tmp;
System.out.println("a=" + a + ",b=" + b);
}

输出:

1
2
复制代码a=1,b=2
a=2,b=1

完美实现交换。但是请注意,这是一道面试题,要的就是考验一些知识点。所以还是老老实实的实现swap方法吧。
有的同学可能会想,Integer 是一个包装类型,是对Int的装箱和拆箱操作。其实也是一个对象。既然是对象,直接更改对象的引用不就行了?
思路没问题,我们首先看看实现:

1
2
3
4
5
6
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

输出:

1
2
3
复制代码a=1,b=2
numa=2,numb=1
a=1,b=2

不出意外,没有成功
这是什么原因呢?
技术老手一看就知道问题出在形参和实参
混淆了

JAVA的形参和实参的区别:

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的numa, numb 就是形参,传递给 swap 方法的 a,b 就是实参

注意:
在值传递调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。
而在引用传递调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上。
那么问题来了,什么是值传递和引用传递

值传递和引用传递

在谈值传递和引用传递之前先了解下 Java的数据类型有哪些

JAVA的数据类型

Java 中的数据类型分为两大类,基本类型和对象类型。相应的,变量也有两种类型:基本类型和引用类型
基本类型的变量保存原始值,即它代表的值就是数值本身,原始值一般对应在内存上的方法区
而引用类型的变量保存引用值,引用值指向内存空间的地址。代表了某个对象的引用,而不是对象本身。对象本身存放在这个引用值所表示的地址的位置。被引用的对象对应内存上的堆内存区。
基本类型包括:byte,short,int,long,char,float,double,boolean 这八大基本数据类型
引用类型包括:类类型,接口类型和数组

变量的基本类型和引用类型的区别

基本数据类型在声明时系统就给它分配空间

1
2
3
复制代码int a;//虽然没有赋值,但声明的时候虚拟机就会 分配 4字节 的内存区域,而引用数据类型不同,它声明时只给变量分配了引用空间,而不分配数据空间:	
String str;//声明的时候没有分配数据空间,只有 4byte 的引用大小,在方法区,而在堆内存区域没有任何分配
str.length(); //这个操作就会报错,因为堆内存上还没有分配内存区域,而 a = 1; 这个操作就不会报错。

好了,Java的数据类型说完了,继续我们的值传递和引用传递的话题。
先背住一个概念:基本类型的变量是值传递;引用类型的变量
结合前面说的 形参和实参。

值传递

方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,
此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值

引用传递

也称为地址传递,址传递。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址
在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象
通过例子来说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码static class Person {
int age;
Person(int age) {
this.age = age;
}
}

private static void test() {
int a = 100;
testValueT(a);
System.out.println("a=" + a);
Person person = new Person(20);
testReference(person);
System.out.println("person.age=" + person.age);
}

private static void testValueT(int a) {
a = 200;
System.out.println("int testValueT a=" + a);
}

private static void testReference(Person person) {
person.age = 10;
}

输出:

1
2
3
复制代码int testValueT a=200
a=100
person.age=10

看见 值传递 a的值并没有改变,而 引用传递的 persion.age已经改变了
有人说

1
2
3
复制代码private static void testReference(Person person) {
person = new Person(100);
}

为什么 输出的 person.age 还是20呢?
我想说 了解一下什么是引用类型吧? 方法内把 形参的地址引用换成了另一个对象,并没有改变这个对象,并不能影响 外边实参还引用原来的对象,因为 形参只在方法内有效哦。

有人或许还有疑问,按照文章开头的例子,Integer也是 引用类型该当如何呢?
其实 类似的 String,Integer,Float,Double,Short,Byte,Long,Character等等基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如Integer 内部有一个value 来记录int基本类型的值,但是没有提供修改它的方法,而且 也是final类型的,无法通过常规手段更改。
所以虽然他们是引用类型的,但是我们可以认为它是值传递,这个也只是认为,事实上还是引用传递,址传递。


好了,基础知识补充完毕,然我们回到面试题吧


回归正题

1
2
3
4
5
6
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
numa = numb;
numb = tmp;
System.out.println("numa=" + numa + ",numb=" + numb);
}

通过补习基础知识,我们很明显知道 上面这个方法实现替换 是不可行的。因为Interger虽然是引用类型
但是上述操作只是改变了形参的引用,而没有改变实参对应的对象。

那么思路来了,我们通过特殊手段改变 Integer内部的value属性

1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
Integer tmp = numa;
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//成功的将numa 引用的 1的对象 值改为 2
field.set(numb, tmp); //由于 tmp 也是指向 numa 未改变前指向的堆 即对象1 ,经过前一步,已经将对象1的值改为了2,自然 numb 也是2,所以改动失效
} catch (Exception e) {
e.printStackTrace();
}
}

输出结果:

1
2
复制代码a=1,b=2
a=2,b=2

又来疑问了?为何 a的值改变成功,而b的改变失败呢?

见代码注释
所以其实 field.set(numb, tmp); 是更改成功的,只是 tmp 经过前一行代码的执行,已经变成了 2。
那么如何破呢?
我们有了一个思路,既然是 tmp的引用的对象值变量,那么我让tmp不引用 numa了

1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();//tmp 定义为基本数据类型
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);//这个时候并不改变 tmp 的值
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

这种情况下 对 numa 这个对象的修改就不会导致 tmp 的值变化了,看一下运行结果

1
2
复制代码a=1,b=2
a=2,b=2

这是为啥?有没有快疯啦?
难道我们的思路错了?
先别着急,我们看看这个例子:
仅仅是将前面的例子 a的值改为 129,b的值改为130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static void main(String[] args) {
Integer a = 129;
Integer b = 130;

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
复制代码a=129,b=130
a=130,b=129

有没有怀疑人生?我们的思路没有问题啊?为什么 换个数值就行了呢?
我们稍微修改一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(2);

System.out.println("a=" + a + ",b=" + b);
swap(a, b);
System.out.println("a=" + a + ",b=" + b);
}

private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, tmp);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果:

1
2
复制代码a=1,b=2
a=2,b=1

哎?为啥 1 和 2 也可以了?
我们这时肯定猜想和Integer的装箱 拆箱有关

装箱,拆箱 概念

Integer的装箱操作

为什么 Integer a = 1 和 Integer a = new Integer(1) 效果不一样
那就瞅瞅源码吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public Integer(int value) {
this.value = value;
}

/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

通过注释知道,java推荐 Integer.valueOf 方式初始化一个Interger因为有 缓存了-128 - 127的数字
我们直接定义 Integer a = 1 具有这个功能,所以 Jvm 底层实现 是通过 Integer.valueOf这个方法
再看 field.set(numb, tmp);
我们打断点,发现通过反射设置
value时 竟然走了 Integer.valueOf 方法
下面是 我们调用 swap前后的 IntegerCache.cache 值得变化

反射修改前:

反射修改后


在反射修改前

1
2
3
复制代码IntegerCache.cache[128]=0
IntegerCache.cache[129]=1
IntegerCache.cache[130]=2

通过反射修改后

1
2
3
复制代码IntegerCache.cache[128]=0
IntegerCache.cache[129]=2
IntegerCache.cache[130]=2

再调用 field.set(numb, tmp) tmp这时等于1 对应的 角标 129 ,但是这个值已经变成了2
所以出现了刚才 奇怪的结果
原来都是缓存的锅
下面趁机再看个例子 加深理解

1
2
3
4
5
6
复制代码Integer testA = 1;
Integer testB = 1;

Integer testC = 128;
Integer testD = 128;
System.out.println("testA=testB " + (testA == testB) + ",\ntestC=testD " + (testC == testD));

输出结果:

1
2
复制代码testA=testB true,
testC=testD false

通过这小示例,在 -128 到 127的数字都走了缓存,这样 testA 和 testB引用的是同一片内存区域的同一个对象。
而 testC testD 数值大于127 所以 没有走缓存,相当于两个Integer对象,在堆内存区域有两个对象。
两个对象自如不相等。
在前面的示例中 我们 通过

1
2
复制代码Integer a = new Integer(1);
Integer b = new Integer(2);

方式初始化 a,b 我们的交换算法没有问题,也是这个原因。

那么到目前为止我们的swap 方法可以完善啦
1
2
3
4
5
6
7
8
9
10
11
复制代码private static void swap(Integer numa, Integer numb) {
int tmp = numa.intValue();
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);
field.set(numa, numb);
field.set(numb, new Integer(tmp));
} catch (Exception e) {
e.printStackTrace();
}
}

只需将之前的 field.set(numb, tmp) 改为 field.set(numb, new Integer(tmp))

到此, 这个面试我们已经通过了,还有一个疑问我没有解答。
为什么 field.set(numb, tmp) 会执行 Integer.valueOf() 而 field.set(numb, new Integer(tmp)) 不会执行。
这就是Integer的装箱操作,当 给 Integer.value 赋值 int时,JVM
检测到 int不是Integer类型,需要装箱,才执行了Integer.valueOf()方法。而field.set(numb, new Integer(tmp)) 设置的 是Integer类型了,就不会再拆箱后再装箱。

Over Thanks

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python Scrapy爬虫框架学习

发表于 2017-11-17

Scrapy 是用Python实现一个为爬取网站数据、提取结构性数据而编写的应用框架。

一、Scrapy框架简介

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

二、架构流程图

接下来的图表展现了Scrapy的架构,包括组件及在系统中发生的数据流的概览(绿色箭头所示)。 下面对每个组件都做了简单介绍,并给出了详细内容的链接。数据流如下所描述。

1、组件

Scrapy Engine

引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。 详细内容查看下面的数据流(Data Flow)部分。

调度器(Scheduler)

调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎。

下载器(Downloader)

下载器负责获取页面数据并提供给引擎,而后提供给spider。

Spiders

Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。 更多内容请看 Spiders 。

Item Pipeline

Item Pipeline负责处理被spider提取出来的item。典型的处理有清理、 验证及持久化(例如存取到数据库中)。 更多内容查看 Item Pipeline 。

下载器中间件(Downloader middlewares)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 下载器中间件(Downloader Middleware) 。

Spider中间件(Spider middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 Spider中间件(Middleware) 。

2、数据流(Data flow)

Scrapy中的数据流由执行引擎控制,其过程如下:

  1. 引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。
  2. 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。
  3. 引擎向调度器请求下一个要爬取的URL。
  4. 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。
  5. 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。
  6. 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。
  7. Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。
  8. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。
  9. (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。

3、事件驱动网络(Event-driven networking)

Scrapy基于事件驱动网络框架 Twisted 编写。因此,Scrapy基于并发性考虑由非阻塞(即异步)的实现。

关于异步编程及Twisted更多的内容请查看下列链接:

三、4步制作爬虫

  1. 新建项目(scrapy startproject xxx):新建一个新的爬虫项目
  2. 明确目标(编写items.py):明确你想要抓取的目标
  3. 制作爬虫(spiders/xxsp der.py):制作爬虫开始爬取网页
  4. 存储内容(pipelines.py):设计管道存储爬取内容

四、安装框架

这里我们使用 conda 来进行安装:

1
复制代码conda install scrapy

或者使用 pip 进行安装:

1
复制代码pip install scrapy

查看安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码➜  spider scrapy -h
Scrapy 1.4.0 - no active project

Usage:
scrapy <command> [options] [args]

Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy

[ more ] More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command

1.创建项目

1
2
3
4
5
6
7
8
复制代码➜  spider scrapy startproject SF
New Scrapy project 'SF', using template directory '/Users/kaiyiwang/anaconda2/lib/python2.7/site-packages/scrapy/templates/project', created in:
/Users/kaiyiwang/Code/python/spider/SF

You can start your first spider with:
cd SF
scrapy genspider example example.com
➜ spider

使用 tree 命令可以查看项目结构:

1
2
3
4
5
6
7
8
9
10
11
复制代码➜  SF tree
.
├── SF
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│   └── __init__.py
└── scrapy.cfg

2.在spiders 目录下创建模板

1
2
3
4
复制代码➜  spiders scrapy genspider sf "https://segmentfault.com"
Created spider 'sf' using template 'basic' in module:
SF.spiders.sf
➜ spiders

这样,就生成了一个项目文件 sf.py

1
2
3
4
5
6
7
8
9
10
11
复制代码# -*- coding: utf-8 -*-
import scrapy


class SfSpider(scrapy.Spider):
name = 'sf'
allowed_domains = ['https://segmentfault.com']
start_urls = ['http://https://segmentfault.com/']

def parse(self, response):
pass

命令:

1
2
3
4
5
复制代码# 测试爬虫是否正常, sf为爬虫的名称
➜ scrapy check sf

# 运行爬虫
➜ scrapy crawl sf

相关文章:

Scrapy入门教程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP 代码复用的方式

发表于 2017-11-17

什么是 Trait?

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 Trait。

  • Trait 是为了单继承语言而准备的一种代码复用机制。
  • Trait 和 Class 相似,它为传统的继承增加了水平的特性的组合,多个无关的 Class 之间不需要互相继承
  • Trait 使得无关的 Class 可以使用相同的属性和方法。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Trait';
}
}

class Base
{
public function index()
{
echo 'index';
}
}

class One extends Base
{
use Test;
}

class Two extends Base
{
use Test;
}

$one = new One();
$two = new Two();

echo $one->echoHello();
echo $one->index();
echo $two->echoHello();

结果输出 Hello Trait index Hello Trait。

从基类继承的成员会被 Trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 Trait 的方法,而 Trait 则覆盖了被继承的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Trait';
}
}

class Base
{
use Test;

public function echoHello()
{
echo 'Hello Base';
}
}

class One extends Base
{
use Test;

public function echoHello()
{
echo 'Hello One';
}
}

class Two extends Base
{
use Test;
}

$one = new One();
$two = new Two();
$base = new Base();

echo $one->echoHello();

echo $two->echoHello();

echo $base->echoHello();

结果输出 Hello One Hello Trait Hello Base。

  • class one 示例覆盖基类和 Trait Test,说明当前类的方法优先级高于他们。
  • class Two 示例覆盖基类,Trait 的有优先级高于继承的基类。
  • class Base 示例覆盖 Trait Test,说明当前类的方法优先级高于 Trait。

通过逗号分隔,在 use 声明列出多个 trait,可以都插入到一个类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello ';
}
}

trait TestTwo
{
public function echoWord()
{
echo 'word !';
}
}


class One
{
use Test,TestTwo;
}

$one = new One();

echo $one->echoHello();
echo $one->echoWord();

结果输出 Hello word !。

如果两个 Trait 都插入了一个同名的方法,如果没有明确解决冲突将会产生一个致命错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello Test';
}

public function echoWord()
{
echo 'word Test';
}
}

trait TestTwo
{
public function echoHello()
{
echo 'Hello TestTwo ';
}

public function echoWord()
{
echo 'word TestTwo';
}
}

class One
{
use Test, TestTwo {
Test::echoHello as echoTest;
Test::echoWord insteadof TestTwo;
TestTwo::echoHello insteadof Test;
}
}

$one = new One();

echo $one->echoTest();
echo $one->echoWord();
echo $one->echoHello();

输出结果:Hello Test word Test Hello TestTwo。

  • 使用 as 作为别名,即 Test::echoHello as echoTest; 输出 Trait Test 中的 echoHello.
  • 使用 insteadof 操作符用来排除掉其他 Trait,即 Test::echoWord insteadof TestTwo; 输出的是 word Test,使用 Trait Test 中的 echoWord

修改 方法的控制权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码<?php

trait Test
{
public function echoHello()
{
echo 'Hello';
}

public function echoWord()
{
echo 'word';
}
}

trait TestTwo
{
public function echoHello()
{
echo 'Hello TestTwo ';
}

public function echoWord()
{
echo 'word TestTwo';
}
}

class One
{
use Test {
echoHello as private;
}
}

class Two
{
use Test {
echoHello as private echoTwo;
}
}

$one = new One();
$two = new Two();

echo $two->echoHello();
  • 输出结果 Hello。
  • class one 中使用 as 将 echoHello 设为私有,则通过 class one 不能访问 echoHello。
  • class two 中使用 as 先将其重新命名,然后将新命名方法设置为私有,原 Trait 中的方法可以正常访问。

Trait 中还可以像类一样定义属性。就是很好用的啦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

citus,一个PostgreSQL的扩展

发表于 2017-11-17

Citus Banner

Build Status Slack Status Latest Docs

What is Citus?

  • Open-source PostgreSQL extension (not a fork)
  • Scalable across multiple machines through sharding and replication
  • Distributed engine for query parallelization
  • Database designed to scale multi-tenant applications

Citus is a distributed database that scales across commodity servers using transparent sharding and replication. Citus extends the underlying database rather than forking it, giving developers and enterprises the power and familiarity of a relational
database. As an extension, Citus supports new PostgreSQL releases, and allows you to benefit from new features while maintaining compatibility with existing PostgreSQL tools.

Citus serves many use cases. Two common ones are:

  1. Multi-tenant database: Most B2B applications already have the notion of a tenant / customer / account built into their data model.
    Citus allows you to scale out your transactional relational database to 100K+ tenants with minimal changes to your application.
  2. Real-time analytics: Citus enables ingesting large volumes of data and running analytical queries on that data in human real-time. Example
    applications include analytic dashboards with subsecond response times and exploratory queries on unfolding events.

To learn more, visit citusdata.com and join the mailing list to stay on top of the latest developments.

Getting started with Citus

The fastest way to get up and running is to create a Citus Cloud account. You can also setup a local Citus cluster with Docker.

Citus Cloud

Citus Cloud runs on top of AWS as a fully managed database as a service and has development plans available for getting started. You can provision a Citus Cloud account at console.citusdata.com and get started with just a few clicks.

Local Citus Cluster

If you’re looking to get started locally, you can follow the following steps to get up and running.

  1. Install Docker Community Edition and Docker Compose
  • Mac:
    1. Download and install Docker.
    2. Start Docker by clicking on the application’s icon.
  • Linux:
1
2
3
4
5
6
复制代码curl -sSL https://get.docker.com/ | sh
sudo usermod -aG docker $USER && exec sg docker newgrp `id -gn`
sudo systemctl start docker

sudo curl -sSL https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

The above version of Docker Compose is sufficient for running Citus, or you can install the latest version.

  1. Pull and start the Docker images
1
2
复制代码curl -sSLO https://raw.githubusercontent.com/citusdata/docker/master/docker-compose.yml
docker-compose -p citus up -d
  1. Connect to the master database
1
复制代码docker exec -it citus_master psql -U postgres
  1. Follow the first tutorial instructions
  2. To shut the cluster down, run
1
复制代码docker-compose -p citus down

Talk to Contributors and Learn More

Documentation Try the Citus tutorial for a hands-on introduction or the documentation for a more comprehensive reference.
Google Groups The Citus Google Group is our place for detailed questions and discussions.
Slack Chat with us in our community Slack channel.
Github Issues We track specific bug reports and feature requests on our project issues.
Twitter Follow @citusdata for general updates and PostgreSQL scaling tips.

Contributing

Citus is built on and of open source, and we welcome your contributions. The CONTRIBUTING.md file explains how to get started developing the Citus extension itself and
our code quality guidelines.

Who is Using Citus?

Citus is deployed in production by many customers, ranging from technology start-ups to large enterprises. Here are some examples:

  • CloudFlare uses Citus to provide real-time analytics on 100 TBs of data from over 4 million customer websites. Case
    Study
  • MixRank uses Citus to efficiently collect and analyze vast amounts of data to allow inside B2B sales teams to find new customers. Case
    Study
  • Neustar builds and maintains scalable ad-tech infrastructure that counts billions of events per day using Citus and HyperLogLog.
  • Agari uses Citus to secure more than 85 percent of U.S. consumer emails on two 6-8 TB clusters. Case
    Study
  • Heap uses Citus to run dynamic funnel, segmentation, and cohort queries across billions of users and tens of billions of events. Watch
    Video

Copyright © 2012–2017 Citus Data, Inc.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

异步加载的基本逻辑与浏览器抓包一般流程

发表于 2017-11-17

本篇内容不涉及任何R语言或者Python代码实现,仅从异步加载的逻辑实现过程以及浏览器抓包分析的角度来给大家分享一下个人近期学习爬虫的一些心得。

涉及到的工具有Chrome浏览器(开发者工具)、postman(一款非常优秀的Chrome网络请求构造工具,你可以在Chrome浏览器在线商店里搜到,也可以下载桌面版)。

1、异步加载概念及实现过程
2、浏览器抓包分析一般流程

异步加载的英文简称是ajax,即“Asynchronous Javascript And XML”(异步JavaScript和XML)是指一种创建交互式网页应用的网页开发技术。它可以在无需重新加载整个网页的情况下,通过在后台与服务器进行局部数据交换,使得网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的特定部分进行更新。

这是百度百科对于异步加载的一般定义,在传统web开发中,使用同步加载模式,更新网页时,所有内容必须重载,导致多请求进程阻塞,网页迟迟无法加载,给web端体验造成很大的伤害。但是异步加载则解决了这个问题,通过异步加载,不仅提高了web端浏览体验,而且减缓了服务器端压力。

但异步加载却给网络数据抓取造成了很大的困难。困难在于,异步加载把所有网络资源分成了两大部分,一部分是静态的html文档(DOM文档),另一部分是嵌入在HTML文档内的js动态脚本。(这里暂时忽略css重叠样式表,它与任务目标几乎没什么联系)。这些js脚本通过<script>元素标签进行引用,是预定义好的js事件函数,我们所说的异步加载便是通过这些js脚本内的事件函数驱动的。

(浏览器在接受静态文档的同时,可以执行js脚本,与服务器交换数据并更新html内的数据块,但是R或者Python这种请求发送终端是无法做到这一点儿的)

这些事件函数内部,从新构建了一系列网络请求,这些网络请求可能是GET类型,也有可能是POST类型,这些请求便是异步加载的核心实现方式——XMLHttpRequest。XHR是由js脚本构建的,而js脚本是由其嵌入html的位置(<script>元素的所处位置)的html动作控制的。这些动作可能是鼠标点击事件、鼠标悬浮事件、下拉菜单、输入框输入查询关键词之后的回车等。

打开浏览器,并通过网址链接到主网页之后,浏览器会自动加载HTML文档,而同时内嵌的js脚本也会通过异步加载方式初始化一部分数据,这些js脚本加载过程与浏览器渲染html的过程并不相互影响。当用户在浏览器界面的特定位置点击或者实施某些html动作时,这些动作会驱动对应位置的js脚本执行其预定义的事件函数,构建XHR请求,这些XHR请求与服务器进行部分数据交互,返回数据之后,再通过回调函数操作对应位置html元素,将数据插入对应位置,至此数据交换的流程结束。


而对于我们这些爬虫学习者而言,其实想要找的就是这些js脚本构建的异步加载请求对象,通过截获这些请求,伪装成浏览器的身份,进而替代浏览器完成数据请求,并获取返回数据。这些异步请求在Chrome的开发者工具中往往都能截获到。

那么在浏览器得开发者工具中,以上所述得各部分又是如何是怎么对应的呢?

打开网易云课堂得主页,按F12进入开发者工具工作台。


Elements模块是浏览器加载后后的带有数据得完整HTML文档。



如何你是使用请求网页的方式来提取数据,那么通常你需要关注得便是这个模块。但是今天我们的主角是异步加载,所以定位到第二个模块——Network,该模块涉及到所有的浏览器与web服务器之间的交互请求记录。在Network模块的all子模块中,是所有请求列表,它包含了请求的所有dom文件、js脚本、css重叠样式表、img文件(图形对象)、Media文件(流媒体文件)、字体文件等。


而在XHR子菜单中,你可以看到这些加载文件中,以异步加载方式进行的文件对象。(xhr就是XMLHttpRequest的缩写),这个栏目将是我们爬虫抓包的主战场,所以要熟练这个界面的所有信息。


在XHR模块的Name列表中,有很多异步加载请求,你需要迅速过滤出我们想要的异步加载请求对象。

这里有一个秘诀!

这些请求对象一般包含两类,一类是.js文件,这些文件是javascript脚本文件,它们是事件驱动函数,是动作中介,尽管所有的异步加载请求都是由它们发起,返回数据也是由它们负责接收并且插入html文档的,但是它们本身并不包含数据,仅仅是一组脚本函数而已。所以在xhr中所有带有js结尾的文件都可以略过。(因为仅就抓包而言,你无须弄清楚这些请求实现的底层过程)。第二类是剩余的那些带有参数的链接、或者是以.json结尾文件。这些对象便是以上所说的js脚本构建的异步加载请求的目标,也是我们想要截获的请求。

针对本例而言,因为之前爬过网易云课堂,所以我心里知道想要的请求对象是studycourse.json,即便不知道,过滤掉js脚本之后,剩余链接中带有参数特征的,或者以json结尾的对象通常就是我们想要找的对象。

怎么验证呢,打开studycourse.json对象(鼠标点击),此时,右侧会出现五个子菜单。一般默认定位在Headers,这里的信息不足以让我们判断,该请求是否是想要的请求,此时你可以定位到第二项——Preview项目中。


当你定位到Preview项目,看到里面的json格式的数据包,打开后里面的内容与我们首页看到的刘凯老师的课程信息一致时,这时候就没错了,十拿九稳了。我们已经截获了想要的请求。



此时再次回到第一个菜单Headers中,Headers中一共四个模块,分别对应我们抓包分析中构造浏览器请求的四大部分。


General模块告诉我们的有用信息主要有两条:

该请求的地址是study.163.com/p/search/st…,这个地址也是我们伪造请求的唯一地址,请求类型是一个POST请求。

Response Headers

该模块是请求的响应报头,也即当请求构造成功之后,反回的数据有关内容。

它告诉我们的最为重要的信息(影响我们爬虫构建过程的)是返回的数据格式(Content-Type:application/json;charset=UTF-8),json返回值决定着我们需要对返回数据使用json的反序列化。(在R中可以使用jsonlite中的fromJSON,在Python中使用json包中的loads.json())。

Requests Headers

该模块是构造请求的请求报头,主要告知我们请求的一些具体信息,期待获取的数据,发送请求的终端类型,以及登录信息,参照页地址等。

重点关注其中的Cookies参数、Content-Type参数、Referer参数、User-Agent参数、edu-script-token参数。

User-Agent是标识请求发送的设备类型(常用于规避服务端反爬,可以伪造合法的终端类型)。Content-Type是请求参数提交的类型,这里是application/json,就是json对象(在R里可以通过jsonlite包的toJSON()函数构造,在Python里使用json.dumps()函数序列化参数)。

Referer是参照页地址,也就是我们在浏览器看到的想要抓取的内容主页。(注意区别与上述所说的抓包需要的请求地址)

edu-script-token是一个重要的随机参数(意味着每打开 一次浏览器都会变动),它应该是当前进程的一个标识(个人理解)。

Cookies是登录状态,用于表明用户登录身份认证。(requests参数虽然有常用的预定义参数,但是不同网站还有会有些独特的参数类型,实际抓包过程需要不断尝试)

Reqests Payload

最后是本次抓包分析的重头戏,查询条件部分。因为数据很多(通常情况下),不可能一次返回,所以我们需要构建一个查询表单,该表单是POST特有的(GET方法的查询参数包含在url中)。这些查询字符串规定了了返回数据中的活动课程 id,课程排序方式,课程作者,每次返回课程数目,页面课程最大数据,每次返回数据时课程偏移量等信息。

1
2
3
4
5
6
7
复制代码activityId:0
keyword:"刘凯"
orderType:5
pageIndex:1
pageSize:20
priceType:-1
searchTimeType:-1

这些表单需要构成一个json序列之后才能上传,R语言中稍微有些曲折,RCurl包中需要借助jsonlite包中的toJSON()函数进行参数序列化,httr包则含有可选的参数编码类型,直接指定即可。Python中的urllib、requests库,则直接通过json包的json.dumps()函数进行json序列化即可。

从新梳理一下:

General模块确定请求URL、请求方式:POST

Requests模块确定Cookies、Content-Type(请求参数提交格式)、Referer(请求定位的参照页)、User-Agent(设备类型)、edu-script-token(当前进程信息)

Resposes模块确定请求返回数据的格式:Content-Type,决定着我们使用什么函数处理返回值。

Request Payload模块决定提交参数信息(提交方式由Requests模块的Content-Type参数决定)。

至此异步加载的流程分析阶段完毕。

下面分享如何使用postman这款请求构造工具进行请求模拟,测试请求参数以及报头信息是否合法,是否可以 正常返回数据。


postman是一款很好用的网络请求模拟构造软件,打开之后,第一部分选择请求类型,第二部分输入请求URL,第三部分输入请求headers,第四部分输入请求的 查询表单体。


在输入body时,记得选择raw单选按钮,并且格式选择JSON(application/json),这是该请求指定的参数提交方式。



以上流程完成之后,可以点击send。

正常的话,在该界面底部就会返回json数据块儿,这些数据块会被自动按照其原格式解析和格式化,json返回值格式化之后如下所示:


在数据块中随便选中一个序列字段,使用Ctrl+F查找功能,查看一下一共有几个,与浏览器中课程数目是否一致。


结果一致,浏览器中的对应页面刚好也是9门课,本次请求构造成功,测试结束,需要构造的请求格式如下:

请求类型:POST
请求资源地址:study.163.com/p/search/st…
请求报头参数:
Content-Type:application/json
edu-script-token:40a297a878e54bdb9440a31345ad5f63
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36

请求查询表单参数:(发送前需要序列化为json对象)

1
2
3
4
5
6
7
8
9
复制代码{
"pageSize":20,
"pageIndex":1,
"searchTimeType":-1,
"orderType":5,
"priceType":-1,
"activityId":0,
"keyword":"刘凯"
}

将以上信息,使用R语言中的RCurl中的postForm函数、httr包中的POST函数,或者Python中的urllib包、requests包均可以模拟构造该请求,详细请求构造过程,不再重复,感兴趣可以参考这几篇文章。

网易云课堂Excel课程爬虫思路

左手用R右手Pyhon系列——趣直播课程抓取实战

Python数据抓取与可视化实战——网易云课堂人工智能与大数据板块课程实战

R语言网络数据抓取的又一个难题,终于攻破了!

R语言爬虫实战——网易云课堂数据分析课程板块数据爬取

R语言爬虫实战——知乎live课程数据爬取实战

在线课程请点击文末原文链接:

Hellobi Live | 9月12日 R语言可视化在商务场景中的应用
往期案例数据请移步本人GitHub:
github.com/ljtyduyu/Da…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP中如何实现Hook机制

发表于 2017-11-17

所谓Hook机制,是从Windows编程中流行开的一种技术。其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,这个钩子并没有实际的意义,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。

笔者在学习钩子机制时,参考的是TP3.2.3的tag和Hook机制,使用的是自己开发的MVC框架,其目录风格等均模仿TP3.2.3,让大佬们见笑了。

举个简单的例子,我们现在要写一个用户注册的功能,如下图:

/App/Home/Controller/IndexController.class.php

突然有一天,客户说我需要增加一个功能,新用户注册奖励50积分,那我只好这样写:

客户看了很满意,但是你的另一个同事需要你的代码,你跟他说自己从git上pull。客户又提出一个要求,要在用户注册后给用户发一个邮件(忍住(╬▔皿▔)),那你会这样做:

如果在一个项目中,有大量的类似修改,你该怎么办?就那么修改?项目只会越来越臃肿,越发的杂乱不堪。捅死客户?别闹了,犯法的ㄟ( ▔, ▔ )ㄏ。辞职?想想房贷,再想想妻儿老小,我忍(。・`ω´・)。ps:程序员哪来的妻儿…( ̄∀ ̄)

言归正传,最好解决办法就是使用钩子机制。

首先来看一下我们写的Hook类:

/CutePHP/Lib/Cute/Hook.class.php

这是我仿照TP的Hook写的一个简单的Hook类,该类中包含了一个静态的私有属性Hooks用于记录所有已经注册的钩子

add方法传入钩子的名称和方法,即可将这个钩子存入数组中,listen则是用于监听某个钩子,只要有这个钩子将调用exec方法执行这个钩子

我们来测试一下,首先在/App/Home/Controller.class.php中埋入钩子:

然后在/App/Home/Plugin目录下面建立和钩子一样的文件夹 /App/Home/Plugin/register 下面建立和钩子名一样的文件register.php,写一个简单的类,名称也叫register,下面有个两个方法,一个叫before一个叫after:

然后在项目的公共配置中注册两个钩子:

/App/Conf/Hook.php

那么当我们访问Home下面Index控制器的Register方法时就会显示:

那么具体的原理究竟是怎么实现的呢?首先大家先来简单的了解一下我的这个框架,在Cute核心类中有个一个Start方法,用于加载路由启动框架,并且在加载控制器之前就先把/App/Conf/Hook.php文件加载进来:

那么在这个文件中,我们的代码是这样的:

大家想一下我们Hook的add方法,是把register这个钩子(类)实例化保存到Hooks这个私有数组中,这一步是在listen方法之前,那么我们再用listen方法去监听这个插件,发现存在,就执行一下这个对象的方法,也就实现了钩子的功能。

PS:文中很多代码和函数都是不存在的,仅用于描述逻辑过程。

PPS:文中使用的是本人自己仿照TP3.2.3写的一个简单MVC框架,仅本人自己使用,过于简陋让各路大佬见笑了。

PPPS:求收藏,求转发。原创不易,搬运注明。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据库中间件TDDL调研笔记 四,TDDL其他特性

发表于 2017-11-17

前篇:

  • 《数据库中间件cobar调研笔记》

13年底负责数据库中间件设计时的调研笔记,拿出来和大家分享,轻拍。

一,TDDL是什么

  • TDDL是Taobao Distribute Data Layer的简称
  • 淘宝一个基于客户端的数据库中间件产品
  • 基于JDBC规范,没有server,以client-jar的形式存在

画外音:数据库中间件有基于服务端的,也有基于客户端的,TDDL属于后者;而cobar是一个中间层服务,使用mysql协议,属于前者。

二,TDDL不支持什么SQL

  • 不支持各类join
  • 不支持多表查询
  • 不支持between/and
  • 不支持not(除了支持not like)
  • 不支持comment,即注释
  • 不支持for update
  • 不支持group by中having后面出现集函数
  • 不支持force index
  • 不支持mysql独有的大部分函数

画外音:分布式数据库中间件,join都是很难支持的,cobar号称的对join的支持即有限,又低效。

三,TDDL支持什么SQL

  • 支持CURD基本语法
  • 支持as
  • 支持表名限定,即”table_name.column”
  • 支持like/not like
  • 支持limit,即mysql的分页语法
  • 支持in
  • 支持嵌套查询,由于不支持多表,只支持单表的嵌套查询

画外音:分布式数据库中间件,支持的语法都很有限,但对于与联网的大数据/高并发应用,足够了,服务层应该做更多的事情。

四,TDDL其他特性

  • 支持oracle和mysql
  • 支持主备动态切换
  • 支持带权重的读写分离
  • 支持分库分表
  • 支持主键生成:oracle用sequence来生成,mysql则需要建立一个用于生成id的表
  • 支持单库事务,不支持夸库事务
  • 支持多库多表分页查询,但会随着翻页,性能降低

画外音:可以看到,其实TDDL很多东西都不支持,那么为什么它还如此流行呢?它解决的根本痛点是“分布式”“分库分表”等。

加入了解决“分布式”“分库分表”的中间件后,SQL功能必然受限,但是,我们应该考虑到:MYSQL的CPU和MEM都是非常珍贵的,我们应该将MYSQL从复杂的计算(事务,JOIN,自查询,存储过程,视图,用户自定义函数,,,)中释放解脱出来,将这些计算迁移到服务层。

当然,有些后台系统或者支撑系统,数据量小或者请求量小,没有“分布式”的需求,为了简化业务逻辑,写了一些复杂的SQL语句,利用了MYSQL的功能,这类系统并不是分布式数据库中间件的潜在用户,也不可能强行让这些系统放弃便利,使用中间件。

五,TDDL层次结构

TDDL是一个客户端jar,它的结构分为三层:

层次 说明 其他
matrix 可以理解为数据源的全部,它由多个group组成
group 可以理解为一个分组,它由多个atom组成
atom 可以理解为一个数据库,可能是读库,也可能是写库

对应上面图例:matrix数据水平分为了两个group,每个group有主备atom组成。

matrix层

  • 核心是规则引擎
  • 实现分库分表
  • 主要路径:sql解析 => 规则引擎计算(路由) => 执行 => 合并结果

group层

  • 读写分离
  • 权重计算
  • 写HA切换
  • 读HA切换
  • 动态新增slave(atom)节点

atom层

  • 单个数据库的抽象;
  • ip /port /user /passwd /connection 动态修改,动态化jboss数据源
  • thread count(线程计数):try catch模式,保护业务处理线程
  • 动态阻止某些sql的执行
  • 执行次数的统计和限制

整个SQL执行过程

  • BEGIN(sql+args),输入是sql和参数
  • sql解析
  • 规则计算
  • 表名替换
  • 选择groupDS执行sql
  • 根据权重选择atomDS
  • 具备重试策略的在atomDS执行sql
  • 读写控制,并发控制,执行sql,返回结果
  • 合并结果集
  • END(ResultSet),输出是结果集

画外音:感觉难点在SQL的解析上。

六,TDDL最佳实践

  • 尽可能使用1对多规则中的1进行数据切分(patition key),例如“用户”就是一个简单好用的纬度
  • 买家卖家的多对多问题,使用数据增量复制的方式冗余数据,进行查询
  • 利用表结构的冗余,减少走网络的次数,买家卖家都存储全部的数据

画外音:这里我展开一下这个使用场景。

以电商的买家卖家为例,业务方既有基于买家的查询需求,又有基于卖家的查询需求,但通常只能以一个纬度进行数据的分库(patition),假设以买家分库, 那卖家的查询需求如何实现呢?

如上图所示:查询买家所有买到的订单及商品可以直接定位到某一个分库,但要查询卖家所有卖出的商品,业务方就必须遍历所有的买家库,然后对结果集进行合并,才能满足需求。

所谓的“数据增量复制”“表结构冗余”“减少网络次数”,是指所有的数据以买家卖家两个纬度冗余存储两份,如下图:

采用一个异步的消息队列机制,将数据以另一个纬度增量复制一份,在查询的时候,可以直接以卖家直接定位到相应的分库。

这种方式有潜在的数据不一致问题。

继续tddl最佳实践:

  • 利用单机资源:单机事务,单机join
  • 存储模型尽量做到以下几点:
  • 尽可能走内存
  • 尽可能将业务要查询的数据物理上放在一起
  • 通过数据冗余,减少网络次数
  • 合理并行,提升响应时间
  • 读瓶颈通过增加slave(atom)解决
  • 写瓶颈通过切分+路由解决

画外音:相比数据库中间件内核,最佳实践与存储模型,对我们有更大的借鉴意义。

七、TDDL的未来?

  • kv是一切数据存取最基本的组成部分
  • 存储节点少做一点,业务代码就要多做一点
  • 想提升查询速度,只有冗余数据一条路可走
  • 类结构化查询语言,对查询来说非常方便

画外音:潜台词是,在大数据量高并发下,SQL不是大势所趋,no-sql和定制化的协议+存储才是未来方向?

13年底的调研笔记,文中的“画外音”是我当时的批注,希望能让大家对TDDL能有一个初步的认识,有疑问之处,欢迎交流。

相关文章:

《数据库中间件cobar调研笔记》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…387388389…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%